From 702f9965abaf87bdd094c7effe5f2dc6f4cdd55f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 5 Apr 2021 15:05:53 -0700 Subject: [PATCH 01/59] Deploy an OpenLDAP server for integration tests Signed-off-by: Andrew Keesler --- test/deploy/tools/ldap.yaml | 229 ++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 test/deploy/tools/ldap.yaml diff --git a/test/deploy/tools/ldap.yaml b/test/deploy/tools/ldap.yaml new file mode 100644 index 00000000..9e178407 --- /dev/null +++ b/test/deploy/tools/ldap.yaml @@ -0,0 +1,229 @@ +#! Copyright 2021 the Pinniped contributors. All Rights Reserved. +#! SPDX-License-Identifier: Apache-2.0 + +#@ load("@ytt:data", "data") +#@ load("@ytt:base64", "base64") +--- +apiVersion: v1 +kind: Secret +metadata: + name: ldap-ldif-files + namespace: tools +type: Opaque +stringData: + #@yaml/text-templated-strings + ldap.ldif: | + # ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! *** + # Here's a good explaination of LDIF: + # https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system + + # pinniped.dev (organization, root) + dn: dc=pinniped,dc=dev + objectClass: dcObject + objectClass: organization + dc: pinniped + o: example + + # users, pinniped.dev (organization unit) + dn: ou=users,dc=pinniped,dc=dev + objectClass: organizationalUnit + ou: users + + # groups, pinniped.dev (organization unit) + dn: ou=groups,dc=pinniped,dc=dev + objectClass: organizationalUnit + ou: groups + + # beach-groups, groups, pinniped.dev (organization unit) + dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev + objectClass: organizationalUnit + ou: beach-groups + + # pinny, users, pinniped.dev (user) + dn: cn=pinny,ou=users,dc=pinniped,dc=dev + objectClass: inetOrgPerson + objectClass: posixAccount + objectClass: shadowAccount + cn: pinny + sn: Seal + givenName: Pinny + mail: pinny.ldap@example.com + userPassword:: (@= base64.encode(data.values.pinny_ldap_password) @) + uid: pinny + uidNumber: 1000 + gidNumber: 1000 + homeDirectory: /home/pinny + loginShell: /bin/bash + gecos: pinny-the-seal + + # wally, users, pinniped.dev (user without password) + dn: cn=wally,ou=users,dc=pinniped,dc=dev + objectClass: inetOrgPerson + objectClass: posixAccount + objectClass: shadowAccount + cn: wally + sn: Walrus + givenName: Wally + mail: wally.ldap@example.com + uid: wally + uidNumber: 1001 + gidNumber: 1001 + homeDirectory: /home/wally + loginShell: /bin/bash + gecos: wally-the-walrus + + # olive, users, pinniped.dev (user without password) + dn: cn=olive,ou=users,dc=pinniped,dc=dev + objectClass: inetOrgPerson + objectClass: posixAccount + objectClass: shadowAccount + cn: olive + sn: Boston Terrier + givenName: Olive + mail: olive.ldap@example.com + uid: olive + uidNumber: 1002 + gidNumber: 1002 + homeDirectory: /home/olive + loginShell: /bin/bash + gecos: olive-the-dog + + # ball-game-players, beach-groups, groups, pinniped.dev (group of users) + dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev + cn: ball-game-players + objectClass: groupOfNames + member: cn=pinny,ou=users,dc=pinniped,dc=dev + member: cn=olive,ou=users,dc=pinniped,dc=dev + + # seals, groups, pinniped.dev (group of users) + dn: cn=seals,ou=groups,dc=pinniped,dc=dev + cn: seals + objectClass: groupOfNames + member: cn=pinny,ou=users,dc=pinniped,dc=dev + + # walruses, groups, pinniped.dev (group of users) + dn: cn=walruses,ou=groups,dc=pinniped,dc=dev + cn: walruses + objectClass: groupOfNames + member: cn=wally,ou=users,dc=pinniped,dc=dev + + # pinnipeds, users, pinniped.dev (group of groups) + dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev + cn: pinnipeds + objectClass: groupOfNames + member: cn=seals,ou=groups,dc=pinniped,dc=dev + member: cn=walruses,ou=groups,dc=pinniped,dc=dev + + # mammals, groups, pinniped.dev (group of both groups and users) + dn: cn=mammals,ou=groups,dc=pinniped,dc=dev + cn: mammals + objectClass: groupOfNames + member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev + member: cn=olive,ou=users,dc=pinniped,dc=dev +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ldap + namespace: tools + labels: + app: ldap +spec: + replicas: 1 + selector: + matchLabels: + app: ldap + template: + metadata: + labels: + app: ldap + spec: + containers: + - name: ldap + #! An issue was reported and will be fixed in bitnami/openldap soon. + image: ghcr.io/pinniped-ci-bot/bitnami-openldap-forked:2.4.58-debian-10-r15 #! our own fork of docker.io/bitnami/openldap + #! image: docker.io/bitnami/openldap + imagePullPolicy: Always + ports: + - name: ldap + containerPort: 1389 + - name: ldaps + containerPort: 1636 + resources: + requests: + cpu: "10m" + memory: "64Mi" + limits: + cpu: "10m" + memory: "64Mi" + readinessProbe: + tcpSocket: + port: ldap + initialDelaySeconds: 25 #! typically takes about 30 seconds to start + timeoutSeconds: 120 + periodSeconds: 5 + failureThreshold: 6 + env: + #! Example ldapsearch commands that can be run from within the container based on these env vars. + #! These will print the whole LDAP tree starting at our root. + #! ldapsearch -x -H 'ldap://ldap.tools.svc.cluster.local' -D 'cn=admin,dc=pinniped,dc=dev' -w password -b 'dc=pinniped,dc=dev' + #! LDAPTLS_CACERT=/var/certs/ca.pem ldapsearch -x -H 'ldaps://ldap.tools.svc.cluster.local' -D 'cn=admin,dc=pinniped,dc=dev' -w password -b 'dc=pinniped,dc=dev' + - name: BITNAMI_DEBUG + value: "true" + - name: LDAP_ADMIN_USERNAME + value: "admin" + - name: LDAP_ADMIN_PASSWORD + value: "password" #! ok to hardcode: the LDAP server will not be available from outside the cluster + - name: LDAP_ENABLE_TLS + value: "yes" + - name: LDAP_TLS_CERT_FILE + value: "/var/certs/ldap.pem" + - name: LDAP_TLS_KEY_FILE + value: "/var/certs/ldap-key.pem" + - name: LDAP_TLS_CA_FILE + value: "/var/certs/ca.pem" + #! This env var was added in our fork to reduce slapd memory consumption from ~700 MB to ~12 MB. + - name: LDAP_ULIMIT_MAX_FILES + value: "1024" + #! Note that the custom LDIF file is only read at pod start-up time. + - name: LDAP_CUSTOM_LDIF_DIR + value: "/var/ldifs" + #! Seems like LDAP_ROOT is still required when using LDAP_CUSTOM_LDIF_DIR because it effects the admin user. + #! Presumably this needs to match the root that we create in the LDIF file. + - name: LDAP_ROOT + value: "dc=pinniped,dc=dev" + volumeMounts: + - name: certs + mountPath: /var/certs + readOnly: true + - name: ldifs + mountPath: /var/ldifs + readOnly: true + volumes: + - name: certs + secret: + secretName: certs + - name: ldifs + secret: + secretName: ldap-ldif-files +--- +apiVersion: v1 +kind: Service +metadata: + name: ldap + namespace: tools + labels: + app: ldap +spec: + type: ClusterIP + selector: + app: ldap + ports: + - protocol: TCP + port: 389 + targetPort: 1389 + name: ldap + - protocol: TCP + port: 636 + targetPort: 1636 + name: ldaps From 2b6859b161de1016ed2c3562813f3509035f4ad3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 6 Apr 2021 13:10:01 -0400 Subject: [PATCH 02/59] Add stub LDAP API type and integration test The goal here was to start on an integration test to get us closer to the red test that we want so we can start working on LDAP. Signed-off-by: Andrew Keesler --- apis/supervisor/idp/v1alpha1/register.go.tmpl | 2 + .../types_ldapidentityprovider.go.tmpl | 65 +++++++ ...or.pinniped.dev_ldapidentityproviders.yaml | 86 +++++++++ deploy/supervisor/z0_crd_overlay.yaml | 9 + generated/1.17/README.adoc | 56 ++++++ .../apis/supervisor/idp/v1alpha1/register.go | 2 + .../v1alpha1/types_ldapidentityprovider.go | 65 +++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 93 +++++++++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../fake/fake_ldapidentityprovider.go | 127 ++++++++++++ .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../idp/v1alpha1/ldapidentityprovider.go | 178 +++++++++++++++++ .../informers/externalversions/generic.go | 2 + .../idp/v1alpha1/interface.go | 7 + .../idp/v1alpha1/ldapidentityprovider.go | 76 ++++++++ .../idp/v1alpha1/expansion_generated.go | 8 + .../idp/v1alpha1/ldapidentityprovider.go | 81 ++++++++ ...or.pinniped.dev_ldapidentityproviders.yaml | 86 +++++++++ generated/1.18/README.adoc | 56 ++++++ .../apis/supervisor/idp/v1alpha1/register.go | 2 + .../v1alpha1/types_ldapidentityprovider.go | 65 +++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 93 +++++++++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../fake/fake_ldapidentityprovider.go | 129 +++++++++++++ .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../idp/v1alpha1/ldapidentityprovider.go | 182 ++++++++++++++++++ .../informers/externalversions/generic.go | 2 + .../idp/v1alpha1/interface.go | 7 + .../idp/v1alpha1/ldapidentityprovider.go | 77 ++++++++ .../idp/v1alpha1/expansion_generated.go | 8 + .../idp/v1alpha1/ldapidentityprovider.go | 81 ++++++++ ...or.pinniped.dev_ldapidentityproviders.yaml | 86 +++++++++ generated/1.19/README.adoc | 56 ++++++ .../apis/supervisor/idp/v1alpha1/register.go | 2 + .../v1alpha1/types_ldapidentityprovider.go | 65 +++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 93 +++++++++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../fake/fake_ldapidentityprovider.go | 129 +++++++++++++ .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../idp/v1alpha1/ldapidentityprovider.go | 182 ++++++++++++++++++ .../informers/externalversions/generic.go | 2 + .../idp/v1alpha1/interface.go | 7 + .../idp/v1alpha1/ldapidentityprovider.go | 77 ++++++++ .../idp/v1alpha1/expansion_generated.go | 8 + .../idp/v1alpha1/ldapidentityprovider.go | 86 +++++++++ ...or.pinniped.dev_ldapidentityproviders.yaml | 86 +++++++++ generated/1.20/README.adoc | 56 ++++++ .../apis/supervisor/idp/v1alpha1/register.go | 2 + .../v1alpha1/types_ldapidentityprovider.go | 65 +++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 93 +++++++++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../fake/fake_ldapidentityprovider.go | 129 +++++++++++++ .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../idp/v1alpha1/ldapidentityprovider.go | 182 ++++++++++++++++++ .../informers/externalversions/generic.go | 2 + .../idp/v1alpha1/interface.go | 7 + .../idp/v1alpha1/ldapidentityprovider.go | 77 ++++++++ .../idp/v1alpha1/expansion_generated.go | 8 + .../idp/v1alpha1/ldapidentityprovider.go | 86 +++++++++ ...or.pinniped.dev_ldapidentityproviders.yaml | 86 +++++++++ .../apis/supervisor/idp/v1alpha1/register.go | 2 + .../v1alpha1/types_ldapidentityprovider.go | 65 +++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 93 +++++++++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../fake/fake_ldapidentityprovider.go | 129 +++++++++++++ .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../idp/v1alpha1/ldapidentityprovider.go | 182 ++++++++++++++++++ .../informers/externalversions/generic.go | 2 + .../idp/v1alpha1/interface.go | 7 + .../idp/v1alpha1/ldapidentityprovider.go | 77 ++++++++ .../idp/v1alpha1/expansion_generated.go | 8 + .../idp/v1alpha1/ldapidentityprovider.go | 86 +++++++++ test/integration/kube_api_discovery_test.go | 14 ++ test/integration/supervisor_login_test.go | 98 +++++++--- test/library/client.go | 37 +++- 80 files changed, 4147 insertions(+), 25 deletions(-) create mode 100644 apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl create mode 100644 deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml create mode 100644 generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go create mode 100644 generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go create mode 100644 generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.17/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml create mode 100644 generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go create mode 100644 generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go create mode 100644 generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.18/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml create mode 100644 generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go create mode 100644 generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go create mode 100644 generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.19/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml create mode 100644 generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go create mode 100644 generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go create mode 100644 generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.20/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml create mode 100644 generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go create mode 100644 generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go create mode 100644 generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go create mode 100644 generated/latest/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go diff --git a/apis/supervisor/idp/v1alpha1/register.go.tmpl b/apis/supervisor/idp/v1alpha1/register.go.tmpl index c03b7dde..ddc9c360 100644 --- a/apis/supervisor/idp/v1alpha1/register.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/register.go.tmpl @@ -32,6 +32,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &OIDCIdentityProvider{}, &OIDCIdentityProviderList{}, + &LDAPIdentityProvider{}, + &LDAPIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl new file mode 100644 index 00000000..4be52014 --- /dev/null +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -0,0 +1,65 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LDAPIdentityProviderPhase string + +const ( + // LDAPPhasePending is the default phase for newly-created LDAPIdentityProvider resources. + LDAPPhasePending LDAPIdentityProviderPhase = "Pending" + + // LDAPPhaseReady is the phase for an LDAPIdentityProvider resource in a healthy state. + LDAPPhaseReady LDAPIdentityProviderPhase = "Ready" + + // LDAPPhaseError is the phase for an LDAPIdentityProvider in an unhealthy state. + LDAPPhaseError LDAPIdentityProviderPhase = "Error" +) + +// Status of an LDAP identity provider. +type LDAPIdentityProviderStatus struct { + // Phase summarizes the overall status of the LDAPIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` +} + +// Spec for configuring an LDAP identity provider. +type LDAPIdentityProviderSpec struct { + // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. + // +kubebuilder:validation:MinLength=1 + Host string `json:"host"` +} + +// LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access +// Protocol (LDAP) identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type LDAPIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec LDAPIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status LDAPIdentityProviderStatus `json:"status,omitempty"` +} + +// List of LDAPIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LDAPIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LDAPIdentityProvider `json:"items"` +} diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml new file mode 100644 index 00000000..e70fb51d --- /dev/null +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -0,0 +1,86 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: ldapidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: LDAPIdentityProvider + listKind: LDAPIdentityProviderList + plural: ldapidentityproviders + singular: ldapidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: LDAPIdentityProvider describes the configuration of an upstream + Lightweight Directory Access Protocol (LDAP) identity provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the identity provider. + properties: + host: + description: 'Host is the hostname of this LDAP identity provider, + i.e., where to connect. For example: ldap.example.com:636.' + minLength: 1 + type: string + required: + - host + type: object + status: + description: Status of the identity provider. + properties: + phase: + default: Pending + description: Phase summarizes the overall status of the LDAPIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/deploy/supervisor/z0_crd_overlay.yaml b/deploy/supervisor/z0_crd_overlay.yaml index c3bb8173..8e2dca11 100644 --- a/deploy/supervisor/z0_crd_overlay.yaml +++ b/deploy/supervisor/z0_crd_overlay.yaml @@ -22,3 +22,12 @@ metadata: name: #@ pinnipedDevAPIGroupWithPrefix("oidcidentityproviders.idp.supervisor") spec: group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + +#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"ldapidentityproviders.idp.supervisor.pinniped.dev"}}), expects=1 +--- +metadata: + #@overlay/match missing_ok=True + labels: #@ labels() + name: #@ pinnipedDevAPIGroupWithPrefix("ldapidentityproviders.idp.supervisor") +spec: + group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 25267e8c..8e0e6fb4 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -688,6 +688,62 @@ Condition status of a resource (mirrored from the metav1.Condition type added in |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] +==== LDAPIdentityProvider + +LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access Protocol (LDAP) identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderlist[$$LDAPIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] +==== LDAPIdentityProviderSpec + +Spec for configuring an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus"] +==== LDAPIdentityProviderStatus + +Status of an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] ==== OIDCAuthorizationConfig diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/register.go b/generated/1.17/apis/supervisor/idp/v1alpha1/register.go index c03b7dde..ddc9c360 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/register.go @@ -32,6 +32,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &OIDCIdentityProvider{}, &OIDCIdentityProviderList{}, + &LDAPIdentityProvider{}, + &LDAPIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go new file mode 100644 index 00000000..4be52014 --- /dev/null +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -0,0 +1,65 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LDAPIdentityProviderPhase string + +const ( + // LDAPPhasePending is the default phase for newly-created LDAPIdentityProvider resources. + LDAPPhasePending LDAPIdentityProviderPhase = "Pending" + + // LDAPPhaseReady is the phase for an LDAPIdentityProvider resource in a healthy state. + LDAPPhaseReady LDAPIdentityProviderPhase = "Ready" + + // LDAPPhaseError is the phase for an LDAPIdentityProvider in an unhealthy state. + LDAPPhaseError LDAPIdentityProviderPhase = "Error" +) + +// Status of an LDAP identity provider. +type LDAPIdentityProviderStatus struct { + // Phase summarizes the overall status of the LDAPIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` +} + +// Spec for configuring an LDAP identity provider. +type LDAPIdentityProviderSpec struct { + // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. + // +kubebuilder:validation:MinLength=1 + Host string `json:"host"` +} + +// LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access +// Protocol (LDAP) identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type LDAPIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec LDAPIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status LDAPIdentityProviderStatus `json:"status,omitempty"` +} + +// List of LDAPIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LDAPIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LDAPIdentityProvider `json:"items"` +} diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b1f0447f..bdd1bc95 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,99 @@ func (in *Condition) DeepCopy() *Condition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProvider. +func (in *LDAPIdentityProvider) DeepCopy() *LDAPIdentityProvider { + if in == nil { + return nil + } + out := new(LDAPIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LDAPIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderList. +func (in *LDAPIdentityProviderList) DeepCopy() *LDAPIdentityProviderList { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderSpec. +func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderStatus. +func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index 5f6bf990..23859d83 100644 --- a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -15,6 +15,10 @@ type FakeIDPV1alpha1 struct { *testing.Fake } +func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { + return &FakeLDAPIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) OIDCIdentityProviders(namespace string) v1alpha1.OIDCIdentityProviderInterface { return &FakeOIDCIdentityProviders{c, namespace} } diff --git a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go new file mode 100644 index 00000000..e78bc2df --- /dev/null +++ b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go @@ -0,0 +1,127 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "go.pinniped.dev/generated/1.17/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeLDAPIdentityProviders implements LDAPIdentityProviderInterface +type FakeLDAPIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var ldapidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "ldapidentityproviders"} + +var ldapidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "LDAPIdentityProvider"} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *FakeLDAPIdentityProviders) Get(name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *FakeLDAPIdentityProviders) List(opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(ldapidentityprovidersResource, ldapidentityprovidersKind, c.ns, opts), &v1alpha1.LDAPIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.LDAPIdentityProviderList{ListMeta: obj.(*v1alpha1.LDAPIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.LDAPIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *FakeLDAPIdentityProviders) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(ldapidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Create(lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Update(lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeLDAPIdentityProviders) UpdateStatus(lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider) (*v1alpha1.LDAPIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(ldapidentityprovidersResource, "status", c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeLDAPIdentityProviders) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeLDAPIdentityProviders) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(ldapidentityprovidersResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.LDAPIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *FakeLDAPIdentityProviders) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(ldapidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} diff --git a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79c7e697..137892f3 100644 --- a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -5,4 +5,6 @@ package v1alpha1 +type LDAPIdentityProviderExpansion interface{} + type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index 85943b30..9176e752 100644 --- a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -13,6 +13,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface + LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -21,6 +22,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { + return newLDAPIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) OIDCIdentityProviders(namespace string) OIDCIdentityProviderInterface { return newOIDCIdentityProviders(c, namespace) } diff --git a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..4410da01 --- /dev/null +++ b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,178 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "time" + + v1alpha1 "go.pinniped.dev/generated/1.17/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.17/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// LDAPIdentityProvidersGetter has a method to return a LDAPIdentityProviderInterface. +// A group's client should implement this interface. +type LDAPIdentityProvidersGetter interface { + LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface +} + +// LDAPIdentityProviderInterface has methods to work with LDAPIdentityProvider resources. +type LDAPIdentityProviderInterface interface { + Create(*v1alpha1.LDAPIdentityProvider) (*v1alpha1.LDAPIdentityProvider, error) + Update(*v1alpha1.LDAPIdentityProvider) (*v1alpha1.LDAPIdentityProvider, error) + UpdateStatus(*v1alpha1.LDAPIdentityProvider) (*v1alpha1.LDAPIdentityProvider, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.LDAPIdentityProvider, error) + List(opts v1.ListOptions) (*v1alpha1.LDAPIdentityProviderList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) + LDAPIdentityProviderExpansion +} + +// lDAPIdentityProviders implements LDAPIdentityProviderInterface +type lDAPIdentityProviders struct { + client rest.Interface + ns string +} + +// newLDAPIdentityProviders returns a LDAPIdentityProviders +func newLDAPIdentityProviders(c *IDPV1alpha1Client, namespace string) *lDAPIdentityProviders { + return &lDAPIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *lDAPIdentityProviders) Get(name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *lDAPIdentityProviders) List(opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.LDAPIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *lDAPIdentityProviders) Watch(opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch() +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Create(lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Body(lDAPIdentityProvider). + Do(). + Into(result) + return +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Update(lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + Body(lDAPIdentityProvider). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *lDAPIdentityProviders) UpdateStatus(lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + SubResource("status"). + Body(lDAPIdentityProvider). + Do(). + Into(result) + return +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *lDAPIdentityProviders) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *lDAPIdentityProviders) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + var timeout time.Duration + if listOptions.TimeoutSeconds != nil { + timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Timeout(timeout). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *lDAPIdentityProviders) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("ldapidentityproviders"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/generated/1.17/client/supervisor/informers/externalversions/generic.go b/generated/1.17/client/supervisor/informers/externalversions/generic.go index 29fd9ef4..f65c952d 100644 --- a/generated/1.17/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.17/client/supervisor/informers/externalversions/generic.go @@ -45,6 +45,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Config().V1alpha1().FederationDomains().Informer()}, nil // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 + case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().OIDCIdentityProviders().Informer()}, nil diff --git a/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 533df089..b7677ddb 100644 --- a/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -11,6 +11,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. + LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. OIDCIdentityProviders() OIDCIdentityProviderInformer } @@ -26,6 +28,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// LDAPIdentityProviders returns a LDAPIdentityProviderInformer. +func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { + return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. func (v *version) OIDCIdentityProviders() OIDCIdentityProviderInformer { return &oIDCIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go b/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..af8c9e06 --- /dev/null +++ b/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,76 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.17/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.17/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.17/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.17/client/supervisor/listers/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderInformer provides access to a shared informer and lister for +// LDAPIdentityProviders. +type LDAPIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.LDAPIdentityProviderLister +} + +type lDAPIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).Watch(options) + }, + }, + &idpv1alpha1.LDAPIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *lDAPIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *lDAPIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.LDAPIdentityProvider{}, f.defaultInformer) +} + +func (f *lDAPIdentityProviderInformer) Lister() v1alpha1.LDAPIdentityProviderLister { + return v1alpha1.NewLDAPIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.17/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.17/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index e754c985..28f41bd7 100644 --- a/generated/1.17/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.17/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -5,6 +5,14 @@ package v1alpha1 +// LDAPIdentityProviderListerExpansion allows custom methods to be added to +// LDAPIdentityProviderLister. +type LDAPIdentityProviderListerExpansion interface{} + +// LDAPIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// LDAPIdentityProviderNamespaceLister. +type LDAPIdentityProviderNamespaceListerExpansion interface{} + // OIDCIdentityProviderListerExpansion allows custom methods to be added to // OIDCIdentityProviderLister. type OIDCIdentityProviderListerExpansion interface{} diff --git a/generated/1.17/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go b/generated/1.17/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..9544d75d --- /dev/null +++ b/generated/1.17/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,81 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.17/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderLister helps list LDAPIdentityProviders. +type LDAPIdentityProviderLister interface { + // List lists all LDAPIdentityProviders in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. + LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister + LDAPIdentityProviderListerExpansion +} + +// lDAPIdentityProviderLister implements the LDAPIdentityProviderLister interface. +type lDAPIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewLDAPIdentityProviderLister returns a new LDAPIdentityProviderLister. +func NewLDAPIdentityProviderLister(indexer cache.Indexer) LDAPIdentityProviderLister { + return &lDAPIdentityProviderLister{indexer: indexer} +} + +// List lists all LDAPIdentityProviders in the indexer. +func (s *lDAPIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. +func (s *lDAPIdentityProviderLister) LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister { + return lDAPIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// LDAPIdentityProviderNamespaceLister helps list and get LDAPIdentityProviders. +type LDAPIdentityProviderNamespaceLister interface { + // List lists all LDAPIdentityProviders in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.LDAPIdentityProvider, error) + LDAPIdentityProviderNamespaceListerExpansion +} + +// lDAPIdentityProviderNamespaceLister implements the LDAPIdentityProviderNamespaceLister +// interface. +type lDAPIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all LDAPIdentityProviders in the indexer for a given namespace. +func (s lDAPIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. +func (s lDAPIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.LDAPIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("ldapidentityprovider"), name) + } + return obj.(*v1alpha1.LDAPIdentityProvider), nil +} diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml new file mode 100644 index 00000000..e70fb51d --- /dev/null +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -0,0 +1,86 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: ldapidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: LDAPIdentityProvider + listKind: LDAPIdentityProviderList + plural: ldapidentityproviders + singular: ldapidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: LDAPIdentityProvider describes the configuration of an upstream + Lightweight Directory Access Protocol (LDAP) identity provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the identity provider. + properties: + host: + description: 'Host is the hostname of this LDAP identity provider, + i.e., where to connect. For example: ldap.example.com:636.' + minLength: 1 + type: string + required: + - host + type: object + status: + description: Status of the identity provider. + properties: + phase: + default: Pending + description: Phase summarizes the overall status of the LDAPIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 2c01f927..cd225272 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -688,6 +688,62 @@ Condition status of a resource (mirrored from the metav1.Condition type added in |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] +==== LDAPIdentityProvider + +LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access Protocol (LDAP) identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderlist[$$LDAPIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] +==== LDAPIdentityProviderSpec + +Spec for configuring an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus"] +==== LDAPIdentityProviderStatus + +Status of an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] ==== OIDCAuthorizationConfig diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/register.go b/generated/1.18/apis/supervisor/idp/v1alpha1/register.go index c03b7dde..ddc9c360 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/register.go @@ -32,6 +32,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &OIDCIdentityProvider{}, &OIDCIdentityProviderList{}, + &LDAPIdentityProvider{}, + &LDAPIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go new file mode 100644 index 00000000..4be52014 --- /dev/null +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -0,0 +1,65 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LDAPIdentityProviderPhase string + +const ( + // LDAPPhasePending is the default phase for newly-created LDAPIdentityProvider resources. + LDAPPhasePending LDAPIdentityProviderPhase = "Pending" + + // LDAPPhaseReady is the phase for an LDAPIdentityProvider resource in a healthy state. + LDAPPhaseReady LDAPIdentityProviderPhase = "Ready" + + // LDAPPhaseError is the phase for an LDAPIdentityProvider in an unhealthy state. + LDAPPhaseError LDAPIdentityProviderPhase = "Error" +) + +// Status of an LDAP identity provider. +type LDAPIdentityProviderStatus struct { + // Phase summarizes the overall status of the LDAPIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` +} + +// Spec for configuring an LDAP identity provider. +type LDAPIdentityProviderSpec struct { + // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. + // +kubebuilder:validation:MinLength=1 + Host string `json:"host"` +} + +// LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access +// Protocol (LDAP) identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type LDAPIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec LDAPIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status LDAPIdentityProviderStatus `json:"status,omitempty"` +} + +// List of LDAPIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LDAPIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LDAPIdentityProvider `json:"items"` +} diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b1f0447f..bdd1bc95 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,99 @@ func (in *Condition) DeepCopy() *Condition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProvider. +func (in *LDAPIdentityProvider) DeepCopy() *LDAPIdentityProvider { + if in == nil { + return nil + } + out := new(LDAPIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LDAPIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderList. +func (in *LDAPIdentityProviderList) DeepCopy() *LDAPIdentityProviderList { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderSpec. +func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderStatus. +func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index 8a54b83d..2c419ceb 100644 --- a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -15,6 +15,10 @@ type FakeIDPV1alpha1 struct { *testing.Fake } +func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { + return &FakeLDAPIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) OIDCIdentityProviders(namespace string) v1alpha1.OIDCIdentityProviderInterface { return &FakeOIDCIdentityProviders{c, namespace} } diff --git a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go new file mode 100644 index 00000000..26a4b77f --- /dev/null +++ b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.18/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeLDAPIdentityProviders implements LDAPIdentityProviderInterface +type FakeLDAPIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var ldapidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "ldapidentityproviders"} + +var ldapidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "LDAPIdentityProvider"} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *FakeLDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *FakeLDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(ldapidentityprovidersResource, ldapidentityprovidersKind, c.ns, opts), &v1alpha1.LDAPIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.LDAPIdentityProviderList{ListMeta: obj.(*v1alpha1.LDAPIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.LDAPIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *FakeLDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(ldapidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeLDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(ldapidentityprovidersResource, "status", c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeLDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeLDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(ldapidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.LDAPIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *FakeLDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(ldapidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} diff --git a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79c7e697..137892f3 100644 --- a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -5,4 +5,6 @@ package v1alpha1 +type LDAPIdentityProviderExpansion interface{} + type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index b6f7504a..fdb10351 100644 --- a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -13,6 +13,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface + LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -21,6 +22,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { + return newLDAPIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) OIDCIdentityProviders(namespace string) OIDCIdentityProviderInterface { return newOIDCIdentityProviders(c, namespace) } diff --git a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..314b5d65 --- /dev/null +++ b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.18/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.18/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// LDAPIdentityProvidersGetter has a method to return a LDAPIdentityProviderInterface. +// A group's client should implement this interface. +type LDAPIdentityProvidersGetter interface { + LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface +} + +// LDAPIdentityProviderInterface has methods to work with LDAPIdentityProvider resources. +type LDAPIdentityProviderInterface interface { + Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.LDAPIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.LDAPIdentityProviderList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) + LDAPIdentityProviderExpansion +} + +// lDAPIdentityProviders implements LDAPIdentityProviderInterface +type lDAPIdentityProviders struct { + client rest.Interface + ns string +} + +// newLDAPIdentityProviders returns a LDAPIdentityProviders +func newLDAPIdentityProviders(c *IDPV1alpha1Client, namespace string) *lDAPIdentityProviders { + return &lDAPIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *lDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *lDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.LDAPIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *lDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *lDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *lDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *lDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *lDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.18/client/supervisor/informers/externalversions/generic.go b/generated/1.18/client/supervisor/informers/externalversions/generic.go index be9458cf..9d15fcd2 100644 --- a/generated/1.18/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.18/client/supervisor/informers/externalversions/generic.go @@ -45,6 +45,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Config().V1alpha1().FederationDomains().Informer()}, nil // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 + case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().OIDCIdentityProviders().Informer()}, nil diff --git a/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 1b7936aa..009ad89c 100644 --- a/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -11,6 +11,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. + LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. OIDCIdentityProviders() OIDCIdentityProviderInformer } @@ -26,6 +28,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// LDAPIdentityProviders returns a LDAPIdentityProviderInformer. +func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { + return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. func (v *version) OIDCIdentityProviders() OIDCIdentityProviderInformer { return &oIDCIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go b/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..a9151158 --- /dev/null +++ b/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.18/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.18/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.18/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.18/client/supervisor/listers/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderInformer provides access to a shared informer and lister for +// LDAPIdentityProviders. +type LDAPIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.LDAPIdentityProviderLister +} + +type lDAPIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.LDAPIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *lDAPIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *lDAPIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.LDAPIdentityProvider{}, f.defaultInformer) +} + +func (f *lDAPIdentityProviderInformer) Lister() v1alpha1.LDAPIdentityProviderLister { + return v1alpha1.NewLDAPIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.18/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.18/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index e754c985..28f41bd7 100644 --- a/generated/1.18/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.18/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -5,6 +5,14 @@ package v1alpha1 +// LDAPIdentityProviderListerExpansion allows custom methods to be added to +// LDAPIdentityProviderLister. +type LDAPIdentityProviderListerExpansion interface{} + +// LDAPIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// LDAPIdentityProviderNamespaceLister. +type LDAPIdentityProviderNamespaceListerExpansion interface{} + // OIDCIdentityProviderListerExpansion allows custom methods to be added to // OIDCIdentityProviderLister. type OIDCIdentityProviderListerExpansion interface{} diff --git a/generated/1.18/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go b/generated/1.18/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..d7957df2 --- /dev/null +++ b/generated/1.18/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,81 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.18/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderLister helps list LDAPIdentityProviders. +type LDAPIdentityProviderLister interface { + // List lists all LDAPIdentityProviders in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. + LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister + LDAPIdentityProviderListerExpansion +} + +// lDAPIdentityProviderLister implements the LDAPIdentityProviderLister interface. +type lDAPIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewLDAPIdentityProviderLister returns a new LDAPIdentityProviderLister. +func NewLDAPIdentityProviderLister(indexer cache.Indexer) LDAPIdentityProviderLister { + return &lDAPIdentityProviderLister{indexer: indexer} +} + +// List lists all LDAPIdentityProviders in the indexer. +func (s *lDAPIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. +func (s *lDAPIdentityProviderLister) LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister { + return lDAPIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// LDAPIdentityProviderNamespaceLister helps list and get LDAPIdentityProviders. +type LDAPIdentityProviderNamespaceLister interface { + // List lists all LDAPIdentityProviders in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.LDAPIdentityProvider, error) + LDAPIdentityProviderNamespaceListerExpansion +} + +// lDAPIdentityProviderNamespaceLister implements the LDAPIdentityProviderNamespaceLister +// interface. +type lDAPIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all LDAPIdentityProviders in the indexer for a given namespace. +func (s lDAPIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. +func (s lDAPIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.LDAPIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("ldapidentityprovider"), name) + } + return obj.(*v1alpha1.LDAPIdentityProvider), nil +} diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml new file mode 100644 index 00000000..e70fb51d --- /dev/null +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -0,0 +1,86 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: ldapidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: LDAPIdentityProvider + listKind: LDAPIdentityProviderList + plural: ldapidentityproviders + singular: ldapidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: LDAPIdentityProvider describes the configuration of an upstream + Lightweight Directory Access Protocol (LDAP) identity provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the identity provider. + properties: + host: + description: 'Host is the hostname of this LDAP identity provider, + i.e., where to connect. For example: ldap.example.com:636.' + minLength: 1 + type: string + required: + - host + type: object + status: + description: Status of the identity provider. + properties: + phase: + default: Pending + description: Phase summarizes the overall status of the LDAPIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 0c2527ae..e20d50b0 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -688,6 +688,62 @@ Condition status of a resource (mirrored from the metav1.Condition type added in |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] +==== LDAPIdentityProvider + +LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access Protocol (LDAP) identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderlist[$$LDAPIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] +==== LDAPIdentityProviderSpec + +Spec for configuring an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus"] +==== LDAPIdentityProviderStatus + +Status of an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] ==== OIDCAuthorizationConfig diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/register.go b/generated/1.19/apis/supervisor/idp/v1alpha1/register.go index c03b7dde..ddc9c360 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/register.go @@ -32,6 +32,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &OIDCIdentityProvider{}, &OIDCIdentityProviderList{}, + &LDAPIdentityProvider{}, + &LDAPIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go new file mode 100644 index 00000000..4be52014 --- /dev/null +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -0,0 +1,65 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LDAPIdentityProviderPhase string + +const ( + // LDAPPhasePending is the default phase for newly-created LDAPIdentityProvider resources. + LDAPPhasePending LDAPIdentityProviderPhase = "Pending" + + // LDAPPhaseReady is the phase for an LDAPIdentityProvider resource in a healthy state. + LDAPPhaseReady LDAPIdentityProviderPhase = "Ready" + + // LDAPPhaseError is the phase for an LDAPIdentityProvider in an unhealthy state. + LDAPPhaseError LDAPIdentityProviderPhase = "Error" +) + +// Status of an LDAP identity provider. +type LDAPIdentityProviderStatus struct { + // Phase summarizes the overall status of the LDAPIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` +} + +// Spec for configuring an LDAP identity provider. +type LDAPIdentityProviderSpec struct { + // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. + // +kubebuilder:validation:MinLength=1 + Host string `json:"host"` +} + +// LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access +// Protocol (LDAP) identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type LDAPIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec LDAPIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status LDAPIdentityProviderStatus `json:"status,omitempty"` +} + +// List of LDAPIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LDAPIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LDAPIdentityProvider `json:"items"` +} diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b1f0447f..bdd1bc95 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,99 @@ func (in *Condition) DeepCopy() *Condition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProvider. +func (in *LDAPIdentityProvider) DeepCopy() *LDAPIdentityProvider { + if in == nil { + return nil + } + out := new(LDAPIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LDAPIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderList. +func (in *LDAPIdentityProviderList) DeepCopy() *LDAPIdentityProviderList { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderSpec. +func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderStatus. +func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index 31ba495f..28e3d63f 100644 --- a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -15,6 +15,10 @@ type FakeIDPV1alpha1 struct { *testing.Fake } +func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { + return &FakeLDAPIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) OIDCIdentityProviders(namespace string) v1alpha1.OIDCIdentityProviderInterface { return &FakeOIDCIdentityProviders{c, namespace} } diff --git a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go new file mode 100644 index 00000000..84f3f24f --- /dev/null +++ b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeLDAPIdentityProviders implements LDAPIdentityProviderInterface +type FakeLDAPIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var ldapidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "ldapidentityproviders"} + +var ldapidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "LDAPIdentityProvider"} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *FakeLDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *FakeLDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(ldapidentityprovidersResource, ldapidentityprovidersKind, c.ns, opts), &v1alpha1.LDAPIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.LDAPIdentityProviderList{ListMeta: obj.(*v1alpha1.LDAPIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.LDAPIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *FakeLDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(ldapidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeLDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(ldapidentityprovidersResource, "status", c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeLDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeLDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(ldapidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.LDAPIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *FakeLDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(ldapidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} diff --git a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79c7e697..137892f3 100644 --- a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -5,4 +5,6 @@ package v1alpha1 +type LDAPIdentityProviderExpansion interface{} + type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index a8c21075..213c0601 100644 --- a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -13,6 +13,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface + LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -21,6 +22,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { + return newLDAPIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) OIDCIdentityProviders(namespace string) OIDCIdentityProviderInterface { return newOIDCIdentityProviders(c, namespace) } diff --git a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..ee93d165 --- /dev/null +++ b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// LDAPIdentityProvidersGetter has a method to return a LDAPIdentityProviderInterface. +// A group's client should implement this interface. +type LDAPIdentityProvidersGetter interface { + LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface +} + +// LDAPIdentityProviderInterface has methods to work with LDAPIdentityProvider resources. +type LDAPIdentityProviderInterface interface { + Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.LDAPIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.LDAPIdentityProviderList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) + LDAPIdentityProviderExpansion +} + +// lDAPIdentityProviders implements LDAPIdentityProviderInterface +type lDAPIdentityProviders struct { + client rest.Interface + ns string +} + +// newLDAPIdentityProviders returns a LDAPIdentityProviders +func newLDAPIdentityProviders(c *IDPV1alpha1Client, namespace string) *lDAPIdentityProviders { + return &lDAPIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *lDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *lDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.LDAPIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *lDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *lDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *lDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *lDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *lDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.19/client/supervisor/informers/externalversions/generic.go b/generated/1.19/client/supervisor/informers/externalversions/generic.go index 4f7a345d..8308f3a9 100644 --- a/generated/1.19/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.19/client/supervisor/informers/externalversions/generic.go @@ -45,6 +45,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Config().V1alpha1().FederationDomains().Informer()}, nil // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 + case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().OIDCIdentityProviders().Informer()}, nil diff --git a/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index a7754336..1b3a9f24 100644 --- a/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -11,6 +11,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. + LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. OIDCIdentityProviders() OIDCIdentityProviderInformer } @@ -26,6 +28,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// LDAPIdentityProviders returns a LDAPIdentityProviderInformer. +func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { + return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. func (v *version) OIDCIdentityProviders() OIDCIdentityProviderInformer { return &oIDCIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go b/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..c9c548d7 --- /dev/null +++ b/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.19/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.19/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.19/client/supervisor/listers/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderInformer provides access to a shared informer and lister for +// LDAPIdentityProviders. +type LDAPIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.LDAPIdentityProviderLister +} + +type lDAPIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.LDAPIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *lDAPIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *lDAPIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.LDAPIdentityProvider{}, f.defaultInformer) +} + +func (f *lDAPIdentityProviderInformer) Lister() v1alpha1.LDAPIdentityProviderLister { + return v1alpha1.NewLDAPIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.19/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.19/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index e754c985..28f41bd7 100644 --- a/generated/1.19/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.19/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -5,6 +5,14 @@ package v1alpha1 +// LDAPIdentityProviderListerExpansion allows custom methods to be added to +// LDAPIdentityProviderLister. +type LDAPIdentityProviderListerExpansion interface{} + +// LDAPIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// LDAPIdentityProviderNamespaceLister. +type LDAPIdentityProviderNamespaceListerExpansion interface{} + // OIDCIdentityProviderListerExpansion allows custom methods to be added to // OIDCIdentityProviderLister. type OIDCIdentityProviderListerExpansion interface{} diff --git a/generated/1.19/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go b/generated/1.19/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..37e5406c --- /dev/null +++ b/generated/1.19/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.19/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderLister helps list LDAPIdentityProviders. +// All objects returned here must be treated as read-only. +type LDAPIdentityProviderLister interface { + // List lists all LDAPIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. + LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister + LDAPIdentityProviderListerExpansion +} + +// lDAPIdentityProviderLister implements the LDAPIdentityProviderLister interface. +type lDAPIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewLDAPIdentityProviderLister returns a new LDAPIdentityProviderLister. +func NewLDAPIdentityProviderLister(indexer cache.Indexer) LDAPIdentityProviderLister { + return &lDAPIdentityProviderLister{indexer: indexer} +} + +// List lists all LDAPIdentityProviders in the indexer. +func (s *lDAPIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. +func (s *lDAPIdentityProviderLister) LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister { + return lDAPIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// LDAPIdentityProviderNamespaceLister helps list and get LDAPIdentityProviders. +// All objects returned here must be treated as read-only. +type LDAPIdentityProviderNamespaceLister interface { + // List lists all LDAPIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.LDAPIdentityProvider, error) + LDAPIdentityProviderNamespaceListerExpansion +} + +// lDAPIdentityProviderNamespaceLister implements the LDAPIdentityProviderNamespaceLister +// interface. +type lDAPIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all LDAPIdentityProviders in the indexer for a given namespace. +func (s lDAPIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. +func (s lDAPIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.LDAPIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("ldapidentityprovider"), name) + } + return obj.(*v1alpha1.LDAPIdentityProvider), nil +} diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml new file mode 100644 index 00000000..e70fb51d --- /dev/null +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -0,0 +1,86 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: ldapidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: LDAPIdentityProvider + listKind: LDAPIdentityProviderList + plural: ldapidentityproviders + singular: ldapidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: LDAPIdentityProvider describes the configuration of an upstream + Lightweight Directory Access Protocol (LDAP) identity provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the identity provider. + properties: + host: + description: 'Host is the hostname of this LDAP identity provider, + i.e., where to connect. For example: ldap.example.com:636.' + minLength: 1 + type: string + required: + - host + type: object + status: + description: Status of the identity provider. + properties: + phase: + default: Pending + description: Phase summarizes the overall status of the LDAPIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 91b4eef6..8edfa8db 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -688,6 +688,62 @@ Condition status of a resource (mirrored from the metav1.Condition type added in |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] +==== LDAPIdentityProvider + +LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access Protocol (LDAP) identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderlist[$$LDAPIdentityProviderList$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.2/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. + +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] +==== LDAPIdentityProviderSpec + +Spec for configuring an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus"] +==== LDAPIdentityProviderStatus + +Status of an LDAP identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovider[$$LDAPIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] ==== OIDCAuthorizationConfig diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/register.go b/generated/1.20/apis/supervisor/idp/v1alpha1/register.go index c03b7dde..ddc9c360 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/register.go @@ -32,6 +32,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &OIDCIdentityProvider{}, &OIDCIdentityProviderList{}, + &LDAPIdentityProvider{}, + &LDAPIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go new file mode 100644 index 00000000..4be52014 --- /dev/null +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -0,0 +1,65 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LDAPIdentityProviderPhase string + +const ( + // LDAPPhasePending is the default phase for newly-created LDAPIdentityProvider resources. + LDAPPhasePending LDAPIdentityProviderPhase = "Pending" + + // LDAPPhaseReady is the phase for an LDAPIdentityProvider resource in a healthy state. + LDAPPhaseReady LDAPIdentityProviderPhase = "Ready" + + // LDAPPhaseError is the phase for an LDAPIdentityProvider in an unhealthy state. + LDAPPhaseError LDAPIdentityProviderPhase = "Error" +) + +// Status of an LDAP identity provider. +type LDAPIdentityProviderStatus struct { + // Phase summarizes the overall status of the LDAPIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` +} + +// Spec for configuring an LDAP identity provider. +type LDAPIdentityProviderSpec struct { + // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. + // +kubebuilder:validation:MinLength=1 + Host string `json:"host"` +} + +// LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access +// Protocol (LDAP) identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type LDAPIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec LDAPIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status LDAPIdentityProviderStatus `json:"status,omitempty"` +} + +// List of LDAPIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LDAPIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LDAPIdentityProvider `json:"items"` +} diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b1f0447f..bdd1bc95 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,99 @@ func (in *Condition) DeepCopy() *Condition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProvider. +func (in *LDAPIdentityProvider) DeepCopy() *LDAPIdentityProvider { + if in == nil { + return nil + } + out := new(LDAPIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LDAPIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderList. +func (in *LDAPIdentityProviderList) DeepCopy() *LDAPIdentityProviderList { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderSpec. +func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderStatus. +func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index a7e12c22..5e927831 100644 --- a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -15,6 +15,10 @@ type FakeIDPV1alpha1 struct { *testing.Fake } +func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { + return &FakeLDAPIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) OIDCIdentityProviders(namespace string) v1alpha1.OIDCIdentityProviderInterface { return &FakeOIDCIdentityProviders{c, namespace} } diff --git a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go new file mode 100644 index 00000000..103c22db --- /dev/null +++ b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeLDAPIdentityProviders implements LDAPIdentityProviderInterface +type FakeLDAPIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var ldapidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "ldapidentityproviders"} + +var ldapidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "LDAPIdentityProvider"} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *FakeLDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *FakeLDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(ldapidentityprovidersResource, ldapidentityprovidersKind, c.ns, opts), &v1alpha1.LDAPIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.LDAPIdentityProviderList{ListMeta: obj.(*v1alpha1.LDAPIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.LDAPIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *FakeLDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(ldapidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeLDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(ldapidentityprovidersResource, "status", c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeLDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeLDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(ldapidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.LDAPIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *FakeLDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(ldapidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} diff --git a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79c7e697..137892f3 100644 --- a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -5,4 +5,6 @@ package v1alpha1 +type LDAPIdentityProviderExpansion interface{} + type OIDCIdentityProviderExpansion interface{} diff --git a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index a5d2d5e7..900f258a 100644 --- a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -13,6 +13,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface + LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -21,6 +22,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { + return newLDAPIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) OIDCIdentityProviders(namespace string) OIDCIdentityProviderInterface { return newOIDCIdentityProviders(c, namespace) } diff --git a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..73a20836 --- /dev/null +++ b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/1.20/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// LDAPIdentityProvidersGetter has a method to return a LDAPIdentityProviderInterface. +// A group's client should implement this interface. +type LDAPIdentityProvidersGetter interface { + LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface +} + +// LDAPIdentityProviderInterface has methods to work with LDAPIdentityProvider resources. +type LDAPIdentityProviderInterface interface { + Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.LDAPIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.LDAPIdentityProviderList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) + LDAPIdentityProviderExpansion +} + +// lDAPIdentityProviders implements LDAPIdentityProviderInterface +type lDAPIdentityProviders struct { + client rest.Interface + ns string +} + +// newLDAPIdentityProviders returns a LDAPIdentityProviders +func newLDAPIdentityProviders(c *IDPV1alpha1Client, namespace string) *lDAPIdentityProviders { + return &lDAPIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *lDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *lDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.LDAPIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *lDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *lDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *lDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *lDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *lDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.20/client/supervisor/informers/externalversions/generic.go b/generated/1.20/client/supervisor/informers/externalversions/generic.go index 5d296a37..b7821644 100644 --- a/generated/1.20/client/supervisor/informers/externalversions/generic.go +++ b/generated/1.20/client/supervisor/informers/externalversions/generic.go @@ -45,6 +45,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Config().V1alpha1().FederationDomains().Informer()}, nil // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 + case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().OIDCIdentityProviders().Informer()}, nil diff --git a/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index d7ceb4c1..34f8361f 100644 --- a/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -11,6 +11,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. + LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. OIDCIdentityProviders() OIDCIdentityProviderInformer } @@ -26,6 +28,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// LDAPIdentityProviders returns a LDAPIdentityProviderInformer. +func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { + return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. func (v *version) OIDCIdentityProviders() OIDCIdentityProviderInformer { return &oIDCIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go b/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..9dfcfc5a --- /dev/null +++ b/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/1.20/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/1.20/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/1.20/client/supervisor/listers/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderInformer provides access to a shared informer and lister for +// LDAPIdentityProviders. +type LDAPIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.LDAPIdentityProviderLister +} + +type lDAPIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.LDAPIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *lDAPIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *lDAPIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.LDAPIdentityProvider{}, f.defaultInformer) +} + +func (f *lDAPIdentityProviderInformer) Lister() v1alpha1.LDAPIdentityProviderLister { + return v1alpha1.NewLDAPIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/1.20/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/1.20/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index e754c985..28f41bd7 100644 --- a/generated/1.20/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/1.20/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -5,6 +5,14 @@ package v1alpha1 +// LDAPIdentityProviderListerExpansion allows custom methods to be added to +// LDAPIdentityProviderLister. +type LDAPIdentityProviderListerExpansion interface{} + +// LDAPIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// LDAPIdentityProviderNamespaceLister. +type LDAPIdentityProviderNamespaceListerExpansion interface{} + // OIDCIdentityProviderListerExpansion allows custom methods to be added to // OIDCIdentityProviderLister. type OIDCIdentityProviderListerExpansion interface{} diff --git a/generated/1.20/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go b/generated/1.20/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..c8215897 --- /dev/null +++ b/generated/1.20/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderLister helps list LDAPIdentityProviders. +// All objects returned here must be treated as read-only. +type LDAPIdentityProviderLister interface { + // List lists all LDAPIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. + LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister + LDAPIdentityProviderListerExpansion +} + +// lDAPIdentityProviderLister implements the LDAPIdentityProviderLister interface. +type lDAPIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewLDAPIdentityProviderLister returns a new LDAPIdentityProviderLister. +func NewLDAPIdentityProviderLister(indexer cache.Indexer) LDAPIdentityProviderLister { + return &lDAPIdentityProviderLister{indexer: indexer} +} + +// List lists all LDAPIdentityProviders in the indexer. +func (s *lDAPIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. +func (s *lDAPIdentityProviderLister) LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister { + return lDAPIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// LDAPIdentityProviderNamespaceLister helps list and get LDAPIdentityProviders. +// All objects returned here must be treated as read-only. +type LDAPIdentityProviderNamespaceLister interface { + // List lists all LDAPIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.LDAPIdentityProvider, error) + LDAPIdentityProviderNamespaceListerExpansion +} + +// lDAPIdentityProviderNamespaceLister implements the LDAPIdentityProviderNamespaceLister +// interface. +type lDAPIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all LDAPIdentityProviders in the indexer for a given namespace. +func (s lDAPIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. +func (s lDAPIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.LDAPIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("ldapidentityprovider"), name) + } + return obj.(*v1alpha1.LDAPIdentityProvider), nil +} diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml new file mode 100644 index 00000000..e70fb51d --- /dev/null +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -0,0 +1,86 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: ldapidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: LDAPIdentityProvider + listKind: LDAPIdentityProviderList + plural: ldapidentityproviders + singular: ldapidentityprovider + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.host + name: Host + type: string + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: LDAPIdentityProvider describes the configuration of an upstream + Lightweight Directory Access Protocol (LDAP) identity provider. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: Spec for configuring the identity provider. + properties: + host: + description: 'Host is the hostname of this LDAP identity provider, + i.e., where to connect. For example: ldap.example.com:636.' + minLength: 1 + type: string + required: + - host + type: object + status: + description: Status of the identity provider. + properties: + phase: + default: Pending + description: Phase summarizes the overall status of the LDAPIdentityProvider. + enum: + - Pending + - Ready + - Error + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/register.go b/generated/latest/apis/supervisor/idp/v1alpha1/register.go index c03b7dde..ddc9c360 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/register.go @@ -32,6 +32,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &OIDCIdentityProvider{}, &OIDCIdentityProviderList{}, + &LDAPIdentityProvider{}, + &LDAPIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go new file mode 100644 index 00000000..4be52014 --- /dev/null +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -0,0 +1,65 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LDAPIdentityProviderPhase string + +const ( + // LDAPPhasePending is the default phase for newly-created LDAPIdentityProvider resources. + LDAPPhasePending LDAPIdentityProviderPhase = "Pending" + + // LDAPPhaseReady is the phase for an LDAPIdentityProvider resource in a healthy state. + LDAPPhaseReady LDAPIdentityProviderPhase = "Ready" + + // LDAPPhaseError is the phase for an LDAPIdentityProvider in an unhealthy state. + LDAPPhaseError LDAPIdentityProviderPhase = "Error" +) + +// Status of an LDAP identity provider. +type LDAPIdentityProviderStatus struct { + // Phase summarizes the overall status of the LDAPIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` +} + +// Spec for configuring an LDAP identity provider. +type LDAPIdentityProviderSpec struct { + // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. + // +kubebuilder:validation:MinLength=1 + Host string `json:"host"` +} + +// LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access +// Protocol (LDAP) identity provider. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:categories=pinniped;pinniped-idp;pinniped-idps +// +kubebuilder:printcolumn:name="Host",type=string,JSONPath=`.spec.host` +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` +// +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +kubebuilder:subresource:status +type LDAPIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec LDAPIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status LDAPIdentityProviderStatus `json:"status,omitempty"` +} + +// List of LDAPIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type LDAPIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []LDAPIdentityProvider `json:"items"` +} diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index b1f0447f..bdd1bc95 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -28,6 +28,99 @@ func (in *Condition) DeepCopy() *Condition { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProvider. +func (in *LDAPIdentityProvider) DeepCopy() *LDAPIdentityProvider { + if in == nil { + return nil + } + out := new(LDAPIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LDAPIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderList. +func (in *LDAPIdentityProviderList) DeepCopy() *LDAPIdentityProviderList { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderSpec. +func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderStatus. +func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go index 70a983b6..c06f7429 100644 --- a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_idp_client.go @@ -15,6 +15,10 @@ type FakeIDPV1alpha1 struct { *testing.Fake } +func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { + return &FakeLDAPIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) OIDCIdentityProviders(namespace string) v1alpha1.OIDCIdentityProviderInterface { return &FakeOIDCIdentityProviders{c, namespace} } diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go new file mode 100644 index 00000000..d3253d48 --- /dev/null +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_ldapidentityprovider.go @@ -0,0 +1,129 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeLDAPIdentityProviders implements LDAPIdentityProviderInterface +type FakeLDAPIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var ldapidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "ldapidentityproviders"} + +var ldapidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "LDAPIdentityProvider"} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *FakeLDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *FakeLDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(ldapidentityprovidersResource, ldapidentityprovidersKind, c.ns, opts), &v1alpha1.LDAPIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.LDAPIdentityProviderList{ListMeta: obj.(*v1alpha1.LDAPIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.LDAPIdentityProviderList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *FakeLDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(ldapidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *FakeLDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(ldapidentityprovidersResource, c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeLDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(ldapidentityprovidersResource, "status", c.ns, lDAPIdentityProvider), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeLDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(ldapidentityprovidersResource, c.ns, name), &v1alpha1.LDAPIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeLDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(ldapidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.LDAPIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *FakeLDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(ldapidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.LDAPIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.LDAPIdentityProvider), err +} diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go index 79c7e697..137892f3 100644 --- a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/generated_expansion.go @@ -5,4 +5,6 @@ package v1alpha1 +type LDAPIdentityProviderExpansion interface{} + type OIDCIdentityProviderExpansion interface{} diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go index b5d9feb7..a32a2dd1 100644 --- a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/idp_client.go @@ -13,6 +13,7 @@ import ( type IDPV1alpha1Interface interface { RESTClient() rest.Interface + LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -21,6 +22,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { + return newLDAPIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) OIDCIdentityProviders(namespace string) OIDCIdentityProviderInterface { return newOIDCIdentityProviders(c, namespace) } diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..c42fee93 --- /dev/null +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,182 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + "time" + + v1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + scheme "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// LDAPIdentityProvidersGetter has a method to return a LDAPIdentityProviderInterface. +// A group's client should implement this interface. +type LDAPIdentityProvidersGetter interface { + LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface +} + +// LDAPIdentityProviderInterface has methods to work with LDAPIdentityProvider resources. +type LDAPIdentityProviderInterface interface { + Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.LDAPIdentityProvider, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.LDAPIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.LDAPIdentityProviderList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) + LDAPIdentityProviderExpansion +} + +// lDAPIdentityProviders implements LDAPIdentityProviderInterface +type lDAPIdentityProviders struct { + client rest.Interface + ns string +} + +// newLDAPIdentityProviders returns a LDAPIdentityProviders +func newLDAPIdentityProviders(c *IDPV1alpha1Client, namespace string) *lDAPIdentityProviders { + return &lDAPIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the lDAPIdentityProvider, and returns the corresponding lDAPIdentityProvider object, and an error if there is any. +func (c *lDAPIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of LDAPIdentityProviders that match those selectors. +func (c *lDAPIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.LDAPIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.LDAPIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested lDAPIdentityProviders. +func (c *lDAPIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a lDAPIdentityProvider and creates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Create(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a lDAPIdentityProvider and updates it. Returns the server's representation of the lDAPIdentityProvider, and an error, if there is any. +func (c *lDAPIdentityProviders) Update(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *lDAPIdentityProviders) UpdateStatus(ctx context.Context, lDAPIdentityProvider *v1alpha1.LDAPIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(lDAPIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(lDAPIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the lDAPIdentityProvider and deletes it. Returns an error if one occurs. +func (c *lDAPIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *lDAPIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("ldapidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched lDAPIdentityProvider. +func (c *lDAPIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.LDAPIdentityProvider, err error) { + result = &v1alpha1.LDAPIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("ldapidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/latest/client/supervisor/informers/externalversions/generic.go b/generated/latest/client/supervisor/informers/externalversions/generic.go index 36a99ea5..338a4d72 100644 --- a/generated/latest/client/supervisor/informers/externalversions/generic.go +++ b/generated/latest/client/supervisor/informers/externalversions/generic.go @@ -45,6 +45,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Config().V1alpha1().FederationDomains().Informer()}, nil // Group=idp.supervisor.pinniped.dev, Version=v1alpha1 + case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().OIDCIdentityProviders().Informer()}, nil diff --git a/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go index 989cfd96..1a1c2d57 100644 --- a/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go +++ b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/interface.go @@ -11,6 +11,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. + LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. OIDCIdentityProviders() OIDCIdentityProviderInformer } @@ -26,6 +28,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// LDAPIdentityProviders returns a LDAPIdentityProviderInformer. +func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { + return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. func (v *version) OIDCIdentityProviders() OIDCIdentityProviderInformer { return &oIDCIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..2e6b3861 --- /dev/null +++ b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,77 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "context" + time "time" + + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + versioned "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" + internalinterfaces "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/internalinterfaces" + v1alpha1 "go.pinniped.dev/generated/latest/client/supervisor/listers/idp/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderInformer provides access to a shared informer and lister for +// LDAPIdentityProviders. +type LDAPIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.LDAPIdentityProviderLister +} + +type lDAPIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredLDAPIdentityProviderInformer constructs a new informer for LDAPIdentityProvider type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredLDAPIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().LDAPIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.LDAPIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *lDAPIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredLDAPIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *lDAPIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.LDAPIdentityProvider{}, f.defaultInformer) +} + +func (f *lDAPIdentityProviderInformer) Lister() v1alpha1.LDAPIdentityProviderLister { + return v1alpha1.NewLDAPIdentityProviderLister(f.Informer().GetIndexer()) +} diff --git a/generated/latest/client/supervisor/listers/idp/v1alpha1/expansion_generated.go b/generated/latest/client/supervisor/listers/idp/v1alpha1/expansion_generated.go index e754c985..28f41bd7 100644 --- a/generated/latest/client/supervisor/listers/idp/v1alpha1/expansion_generated.go +++ b/generated/latest/client/supervisor/listers/idp/v1alpha1/expansion_generated.go @@ -5,6 +5,14 @@ package v1alpha1 +// LDAPIdentityProviderListerExpansion allows custom methods to be added to +// LDAPIdentityProviderLister. +type LDAPIdentityProviderListerExpansion interface{} + +// LDAPIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// LDAPIdentityProviderNamespaceLister. +type LDAPIdentityProviderNamespaceListerExpansion interface{} + // OIDCIdentityProviderListerExpansion allows custom methods to be added to // OIDCIdentityProviderLister. type OIDCIdentityProviderListerExpansion interface{} diff --git a/generated/latest/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go b/generated/latest/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go new file mode 100644 index 00000000..4c7d773c --- /dev/null +++ b/generated/latest/client/supervisor/listers/idp/v1alpha1/ldapidentityprovider.go @@ -0,0 +1,86 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// LDAPIdentityProviderLister helps list LDAPIdentityProviders. +// All objects returned here must be treated as read-only. +type LDAPIdentityProviderLister interface { + // List lists all LDAPIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. + LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister + LDAPIdentityProviderListerExpansion +} + +// lDAPIdentityProviderLister implements the LDAPIdentityProviderLister interface. +type lDAPIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewLDAPIdentityProviderLister returns a new LDAPIdentityProviderLister. +func NewLDAPIdentityProviderLister(indexer cache.Indexer) LDAPIdentityProviderLister { + return &lDAPIdentityProviderLister{indexer: indexer} +} + +// List lists all LDAPIdentityProviders in the indexer. +func (s *lDAPIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// LDAPIdentityProviders returns an object that can list and get LDAPIdentityProviders. +func (s *lDAPIdentityProviderLister) LDAPIdentityProviders(namespace string) LDAPIdentityProviderNamespaceLister { + return lDAPIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// LDAPIdentityProviderNamespaceLister helps list and get LDAPIdentityProviders. +// All objects returned here must be treated as read-only. +type LDAPIdentityProviderNamespaceLister interface { + // List lists all LDAPIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) + // Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.LDAPIdentityProvider, error) + LDAPIdentityProviderNamespaceListerExpansion +} + +// lDAPIdentityProviderNamespaceLister implements the LDAPIdentityProviderNamespaceLister +// interface. +type lDAPIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all LDAPIdentityProviders in the indexer for a given namespace. +func (s lDAPIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.LDAPIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.LDAPIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the LDAPIdentityProvider from the indexer for a given namespace and name. +func (s lDAPIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.LDAPIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("ldapidentityprovider"), name) + } + return obj.(*v1alpha1.LDAPIdentityProvider), nil +} diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index b7a652a2..c0917fdc 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -170,6 +170,20 @@ func TestGetAPIResourceList(t *testing.T) { Kind: "OIDCIdentityProvider", Verbs: []string{"get", "patch", "update"}, }, + { + Name: "ldapidentityproviders", + SingularName: "ldapidentityprovider", + Namespaced: true, + Kind: "LDAPIdentityProvider", + Verbs: []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}, + Categories: []string{"pinniped", "pinniped-idp", "pinniped-idps"}, + }, + { + Name: "ldapidentityproviders/status", + Namespaced: true, + Kind: "LDAPIdentityProvider", + Verbs: []string{"get", "patch", "update"}, + }, }, }, }, diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 962895da..3af75826 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -17,12 +17,11 @@ import ( "testing" "time" - v1 "k8s.io/api/core/v1" - coreosoidc "github.com/coreos/go-oidc/v3/oidc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" + v1 "k8s.io/api/core/v1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" @@ -37,6 +36,52 @@ import ( ) func TestSupervisorLogin(t *testing.T) { + tests := []struct { + name string + createIDP func(t *testing.T) + requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string) + }{ + { + name: "oidc", + createIDP: func(t *testing.T) { + t.Helper() + env := library.IntegrationEnv(t) + library.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: env.SupervisorTestUpstream.Issuer, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)), + }, + Client: idpv1alpha1.OIDCClient{ + SecretName: library.CreateClientCredsSecret(t, env.SupervisorTestUpstream.ClientID, env.SupervisorTestUpstream.ClientSecret).Name, + }, + }, idpv1alpha1.PhaseReady) + }, + requestAuthorization: requestAuthorizationUsingOIDCIdentityProvider, + }, + { + name: "ldap", + createIDP: func(t *testing.T) { + t.Helper() + library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ + Host: "something", + }, "") // TODO: this should be Ready! + }, + requestAuthorization: requestAuthorizationUsingLDAPIdentityProvider, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + testSupervisorLogin(t, test.createIDP, test.requestAuthorization) + }) + } +} + +func testSupervisorLogin( + t *testing.T, + createIDP func(t *testing.T), + requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string), +) { env := library.IntegrationEnv(t) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) @@ -119,16 +164,8 @@ func TestSupervisorLogin(t *testing.T) { }, 30*time.Second, 200*time.Millisecond) require.Equal(t, http.StatusOK, jwksRequestStatus) - // Create upstream OIDC provider and wait for it to become ready. - library.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorTestUpstream.Issuer, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)), - }, - Client: idpv1alpha1.OIDCClient{ - SecretName: library.CreateClientCredsSecret(t, env.SupervisorTestUpstream.ClientID, env.SupervisorTestUpstream.ClientSecret).Name, - }, - }, idpv1alpha1.PhaseReady) + // Create upstream IDP and wait for it to become ready. + createIDP(t) // Perform OIDC discovery for our downstream. var discovery *coreosoidc.Provider @@ -172,18 +209,8 @@ func TestSupervisorLogin(t *testing.T) { require.NoError(t, authorizeResp.Body.Close()) expectSecurityHeaders(t, authorizeResp) - // Open the web browser and navigate to the downstream authorize URL. - page := browsertest.Open(t) - t.Logf("opening browser to downstream authorize URL %s", library.MaskTokens(downstreamAuthorizeURL)) - require.NoError(t, page.Navigate(downstreamAuthorizeURL)) - - // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorTestUpstream) - - // Wait for the login to happen and us be redirected back to a localhost callback. - t.Logf("waiting for redirect to callback") - callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(localCallbackServer.URL) + `\?.+\z`) - browsertest.WaitForURL(t, page, callbackURLPattern) + // Perform parameterized auth code acquisition. + requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL) // Expect that our callback handler was invoked. callback := localCallbackServer.waitForCallback(10 * time.Second) @@ -269,6 +296,29 @@ func verifyTokenResponse( require.NotEmpty(t, tokenResponse.RefreshToken) } +func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string) { + t.Helper() + env := library.IntegrationEnv(t) + + // Open the web browser and navigate to the downstream authorize URL. + page := browsertest.Open(t) + t.Logf("opening browser to downstream authorize URL %s", library.MaskTokens(downstreamAuthorizeURL)) + require.NoError(t, page.Navigate(downstreamAuthorizeURL)) + + // Expect to be redirected to the upstream provider and log in. + browsertest.LoginToUpstream(t, page, env.SupervisorTestUpstream) + + // Wait for the login to happen and us be redirected back to a localhost callback. + t.Logf("waiting for redirect to callback") + callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(downstreamCallbackURL) + `\?.+\z`) + browsertest.WaitForURL(t, page, callbackURLPattern) +} + +func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string) { + t.Helper() + t.Skip("implement me!") +} + func startLocalCallbackServer(t *testing.T) *localCallbackServer { // Handle the callback by sending the *http.Request object back through a channel. callbacks := make(chan *http.Request, 1) diff --git a/test/library/client.go b/test/library/client.go index 789ffb2d..acbf7626 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -377,7 +377,7 @@ func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityP upstreams := client.IDPV1alpha1().OIDCIdentityProviders(env.SupervisorNamespace) created, err := upstreams.Create(ctx, &idpv1alpha1.OIDCIdentityProvider{ - ObjectMeta: testObjectMeta(t, "upstream"), + ObjectMeta: testObjectMeta(t, "upstream-oidc-idp"), Spec: spec, }, metav1.CreateOptions{}) require.NoError(t, err) @@ -401,6 +401,41 @@ func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityP return result } +func CreateTestLDAPIdentityProvider(t *testing.T, spec idpv1alpha1.LDAPIdentityProviderSpec, expectedPhase idpv1alpha1.LDAPIdentityProviderPhase) *idpv1alpha1.LDAPIdentityProvider { + t.Helper() + env := IntegrationEnv(t) + client := 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) + + created, err := upstreams.Create(ctx, &idpv1alpha1.LDAPIdentityProvider{ + ObjectMeta: testObjectMeta(t, "upstream-ldap-idp"), + Spec: spec, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + // Always clean this up after this point. + t.Cleanup(func() { + t.Logf("cleaning up test LDAPIdentityProvider %s/%s", created.Namespace, created.Name) + err := upstreams.Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + t.Logf("created test LDAPIdentityProvider %s", created.Name) + + // Wait for the LDAPIdentityProvider to enter the expected phase (or time out). + var result *idpv1alpha1.LDAPIdentityProvider + require.Eventuallyf(t, func() bool { + var err error + result, err = upstreams.Get(ctx, created.Name, metav1.GetOptions{}) + require.NoError(t, err) + return result.Status.Phase == expectedPhase + }, 60*time.Second, 1*time.Second, "expected the LDAPIdentityProvider to go into phase %s", expectedPhase) + return result +} + func CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef rbacv1.RoleRef) *rbacv1.ClusterRoleBinding { t.Helper() client := NewKubernetesClientset(t) From 1c55c857f49a6a2f930c0c0b71b6980e818d8037 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 7 Apr 2021 12:56:09 -0700 Subject: [PATCH 03/59] Start to fill out LDAPIdentityProvider's fields and TestSupervisorLogin - Add some fields to LDAPIdentityProvider that we will need to be able to search for users during login - Enhance TestSupervisorLogin to test logging in using an upstream LDAP identity provider. Part of this new test is skipped for now because we haven't written the corresponding production code to make it pass yet. - Some refactoring and enhancement to env.go and the corresponding env vars to support the new upstream LDAP provider integration tests. - Use docker.io/bitnami/openldap for our test LDAP server instead of our own fork now that they have fixed the bug that we reported. Signed-off-by: Andrew Keesler --- .../types_ldapidentityprovider.go.tmpl | 58 ++++++++ ...or.pinniped.dev_ldapidentityproviders.yaml | 69 +++++++++ generated/1.17/README.adoc | 74 ++++++++++ .../v1alpha1/types_ldapidentityprovider.go | 58 ++++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 74 +++++++++- ...or.pinniped.dev_ldapidentityproviders.yaml | 69 +++++++++ generated/1.18/README.adoc | 74 ++++++++++ .../v1alpha1/types_ldapidentityprovider.go | 58 ++++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 74 +++++++++- ...or.pinniped.dev_ldapidentityproviders.yaml | 69 +++++++++ generated/1.19/README.adoc | 74 ++++++++++ .../v1alpha1/types_ldapidentityprovider.go | 58 ++++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 74 +++++++++- ...or.pinniped.dev_ldapidentityproviders.yaml | 69 +++++++++ generated/1.20/README.adoc | 74 ++++++++++ .../v1alpha1/types_ldapidentityprovider.go | 58 ++++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 74 +++++++++- ...or.pinniped.dev_ldapidentityproviders.yaml | 69 +++++++++ .../v1alpha1/types_ldapidentityprovider.go | 58 ++++++++ .../idp/v1alpha1/zz_generated.deepcopy.go | 74 +++++++++- hack/prepare-for-integration-tests.sh | 5 +- test/deploy/tools/ldap.yaml | 7 +- test/integration/cli_test.go | 26 ++-- test/integration/e2e_test.go | 36 ++--- test/integration/supervisor_login_test.go | 136 ++++++++++++++---- test/integration/supervisor_upstream_test.go | 4 +- test/library/client.go | 13 +- test/library/env.go | 39 ++++- 28 files changed, 1537 insertions(+), 88 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index 4be52014..5e602f31 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -28,11 +28,69 @@ type LDAPIdentityProviderStatus struct { Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` } +type LDAPIdentityProviderTLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. + // If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} + +type LDAPIdentityProviderBindSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be + // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value + // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type LDAPIdentityProviderUserSearchAttributesSpec struct { + // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // +kubebuilder:validation:MinLength=1 + Username string `json:"username,omitempty"` + + // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // +kubebuilder:validation:MinLength=1 + UniqueID string `json:"uniqueID,omitempty"` +} + +type LDAPIdentityProviderUserSearchSpec struct { + // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + Base string `json:"base,omitempty"` + + // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" + // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Optional. When not specified, the default will act as if the Filter were specified as the value from + // Attributes.Username appended by "={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the LDAP entry which was found as + // the result of the user search. + // +optional + Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. // +kubebuilder:validation:MinLength=1 Host string `json:"host"` + + // TLS contains the connection settings for how to establish the connection to the Host. + TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in the LDAP provider. + UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index e70fb51d..1e54d043 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -51,11 +51,80 @@ spec: spec: description: Spec for configuring the identity provider. properties: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the LDAP server to be allowed + to perform searches and binds to validate a user's credentials during + a user's authentication attempt. + properties: + secretName: + description: SecretName contains the name of a namespace-local + Secret object that provides the username and password for an + LDAP bind user. This account will be used to perform LDAP searches. + The Secret should be of type "kubernetes.io/basic-auth" which + includes "username" and "password" keys. The username value + should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + minLength: 1 + type: string + required: + - secretName + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' minLength: 1 type: string + tls: + description: TLS contains the connection settings for how to establish + the connection to the Host. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle) + to trust when connecting to the LDAP provider. If omitted, a + default set of system roots will be trusted. + type: string + type: object + userSearch: + description: UserSearch contains the configuration for searching for + a user by name in the LDAP provider. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the LDAP entry which was found as the result of + the user search. + properties: + uniqueID: + description: UniqueID specifies the name of the attribute + in the LDAP entry which whose value shall be used to uniquely + identify the user within this LDAP provider after a successful + authentication. E.g. "uidNumber" or "objectGUID". + minLength: 1 + type: string + username: + description: Username specifies the name of attribute in the + LDAP entry which whose value shall become the username of + the user after a successful authentication. This would typically + be the same attribute name used in the user search filter. + E.g. "mail" or "uid" or "userPrincipalName". + minLength: 1 + type: string + type: object + base: + description: Base is the DN that should be used as the search + base when searching for users. E.g. "ou=users,dc=example,dc=com". + minLength: 1 + type: string + filter: + description: Filter is the LDAP search filter which should be + applied when searching for users. The pattern "{}" must occur + in the filter and will be dynamically replaced by the username + for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". + For more information about LDAP filters, see https://ldap.com/ldap-filters. + Optional. When not specified, the default will act as if the + Filter were specified as the value from Attributes.Username + appended by "={}". + type: string + type: object required: - host type: object diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 8e0e6fb4..dec89a34 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -708,6 +708,23 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec"] +==== LDAPIdentityProviderBindSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] @@ -724,6 +741,9 @@ Spec for configuring an LDAP identity provider. |=== | Field | Description | *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. |=== @@ -744,6 +764,60 @@ Status of an LDAP identity provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec"] +==== LDAPIdentityProviderTLSSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. If omitted, a default set of system roots will be trusted. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec"] +==== LDAPIdentityProviderUserSearchAttributesSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec"] +==== LDAPIdentityProviderUserSearchSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] ==== OIDCAuthorizationConfig diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 4be52014..5e602f31 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -28,11 +28,69 @@ type LDAPIdentityProviderStatus struct { Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` } +type LDAPIdentityProviderTLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. + // If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} + +type LDAPIdentityProviderBindSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be + // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value + // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type LDAPIdentityProviderUserSearchAttributesSpec struct { + // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // +kubebuilder:validation:MinLength=1 + Username string `json:"username,omitempty"` + + // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // +kubebuilder:validation:MinLength=1 + UniqueID string `json:"uniqueID,omitempty"` +} + +type LDAPIdentityProviderUserSearchSpec struct { + // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + Base string `json:"base,omitempty"` + + // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" + // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Optional. When not specified, the default will act as if the Filter were specified as the value from + // Attributes.Username appended by "={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the LDAP entry which was found as + // the result of the user search. + // +optional + Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. // +kubebuilder:validation:MinLength=1 Host string `json:"host"` + + // TLS contains the connection settings for how to establish the connection to the Host. + TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in the LDAP provider. + UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index bdd1bc95..6ddcebad 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -33,7 +33,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -56,6 +56,22 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. +func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderBindSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -92,6 +108,13 @@ func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(LDAPIdentityProviderTLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch return } @@ -121,6 +144,55 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. +func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderTLSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index e70fb51d..1e54d043 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -51,11 +51,80 @@ spec: spec: description: Spec for configuring the identity provider. properties: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the LDAP server to be allowed + to perform searches and binds to validate a user's credentials during + a user's authentication attempt. + properties: + secretName: + description: SecretName contains the name of a namespace-local + Secret object that provides the username and password for an + LDAP bind user. This account will be used to perform LDAP searches. + The Secret should be of type "kubernetes.io/basic-auth" which + includes "username" and "password" keys. The username value + should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + minLength: 1 + type: string + required: + - secretName + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' minLength: 1 type: string + tls: + description: TLS contains the connection settings for how to establish + the connection to the Host. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle) + to trust when connecting to the LDAP provider. If omitted, a + default set of system roots will be trusted. + type: string + type: object + userSearch: + description: UserSearch contains the configuration for searching for + a user by name in the LDAP provider. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the LDAP entry which was found as the result of + the user search. + properties: + uniqueID: + description: UniqueID specifies the name of the attribute + in the LDAP entry which whose value shall be used to uniquely + identify the user within this LDAP provider after a successful + authentication. E.g. "uidNumber" or "objectGUID". + minLength: 1 + type: string + username: + description: Username specifies the name of attribute in the + LDAP entry which whose value shall become the username of + the user after a successful authentication. This would typically + be the same attribute name used in the user search filter. + E.g. "mail" or "uid" or "userPrincipalName". + minLength: 1 + type: string + type: object + base: + description: Base is the DN that should be used as the search + base when searching for users. E.g. "ou=users,dc=example,dc=com". + minLength: 1 + type: string + filter: + description: Filter is the LDAP search filter which should be + applied when searching for users. The pattern "{}" must occur + in the filter and will be dynamically replaced by the username + for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". + For more information about LDAP filters, see https://ldap.com/ldap-filters. + Optional. When not specified, the default will act as if the + Filter were specified as the value from Attributes.Username + appended by "={}". + type: string + type: object required: - host type: object diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index cd225272..6609724e 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -708,6 +708,23 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec"] +==== LDAPIdentityProviderBindSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] @@ -724,6 +741,9 @@ Spec for configuring an LDAP identity provider. |=== | Field | Description | *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. |=== @@ -744,6 +764,60 @@ Status of an LDAP identity provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec"] +==== LDAPIdentityProviderTLSSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. If omitted, a default set of system roots will be trusted. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec"] +==== LDAPIdentityProviderUserSearchAttributesSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec"] +==== LDAPIdentityProviderUserSearchSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] ==== OIDCAuthorizationConfig diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 4be52014..5e602f31 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -28,11 +28,69 @@ type LDAPIdentityProviderStatus struct { Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` } +type LDAPIdentityProviderTLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. + // If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} + +type LDAPIdentityProviderBindSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be + // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value + // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type LDAPIdentityProviderUserSearchAttributesSpec struct { + // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // +kubebuilder:validation:MinLength=1 + Username string `json:"username,omitempty"` + + // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // +kubebuilder:validation:MinLength=1 + UniqueID string `json:"uniqueID,omitempty"` +} + +type LDAPIdentityProviderUserSearchSpec struct { + // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + Base string `json:"base,omitempty"` + + // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" + // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Optional. When not specified, the default will act as if the Filter were specified as the value from + // Attributes.Username appended by "={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the LDAP entry which was found as + // the result of the user search. + // +optional + Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. // +kubebuilder:validation:MinLength=1 Host string `json:"host"` + + // TLS contains the connection settings for how to establish the connection to the Host. + TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in the LDAP provider. + UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index bdd1bc95..6ddcebad 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -33,7 +33,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -56,6 +56,22 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. +func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderBindSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -92,6 +108,13 @@ func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(LDAPIdentityProviderTLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch return } @@ -121,6 +144,55 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. +func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderTLSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index e70fb51d..1e54d043 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -51,11 +51,80 @@ spec: spec: description: Spec for configuring the identity provider. properties: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the LDAP server to be allowed + to perform searches and binds to validate a user's credentials during + a user's authentication attempt. + properties: + secretName: + description: SecretName contains the name of a namespace-local + Secret object that provides the username and password for an + LDAP bind user. This account will be used to perform LDAP searches. + The Secret should be of type "kubernetes.io/basic-auth" which + includes "username" and "password" keys. The username value + should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + minLength: 1 + type: string + required: + - secretName + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' minLength: 1 type: string + tls: + description: TLS contains the connection settings for how to establish + the connection to the Host. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle) + to trust when connecting to the LDAP provider. If omitted, a + default set of system roots will be trusted. + type: string + type: object + userSearch: + description: UserSearch contains the configuration for searching for + a user by name in the LDAP provider. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the LDAP entry which was found as the result of + the user search. + properties: + uniqueID: + description: UniqueID specifies the name of the attribute + in the LDAP entry which whose value shall be used to uniquely + identify the user within this LDAP provider after a successful + authentication. E.g. "uidNumber" or "objectGUID". + minLength: 1 + type: string + username: + description: Username specifies the name of attribute in the + LDAP entry which whose value shall become the username of + the user after a successful authentication. This would typically + be the same attribute name used in the user search filter. + E.g. "mail" or "uid" or "userPrincipalName". + minLength: 1 + type: string + type: object + base: + description: Base is the DN that should be used as the search + base when searching for users. E.g. "ou=users,dc=example,dc=com". + minLength: 1 + type: string + filter: + description: Filter is the LDAP search filter which should be + applied when searching for users. The pattern "{}" must occur + in the filter and will be dynamically replaced by the username + for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". + For more information about LDAP filters, see https://ldap.com/ldap-filters. + Optional. When not specified, the default will act as if the + Filter were specified as the value from Attributes.Username + appended by "={}". + type: string + type: object required: - host type: object diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index e20d50b0..e9c1538e 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -708,6 +708,23 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec"] +==== LDAPIdentityProviderBindSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] @@ -724,6 +741,9 @@ Spec for configuring an LDAP identity provider. |=== | Field | Description | *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. |=== @@ -744,6 +764,60 @@ Status of an LDAP identity provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec"] +==== LDAPIdentityProviderTLSSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. If omitted, a default set of system roots will be trusted. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec"] +==== LDAPIdentityProviderUserSearchAttributesSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec"] +==== LDAPIdentityProviderUserSearchSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] ==== OIDCAuthorizationConfig diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 4be52014..5e602f31 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -28,11 +28,69 @@ type LDAPIdentityProviderStatus struct { Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` } +type LDAPIdentityProviderTLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. + // If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} + +type LDAPIdentityProviderBindSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be + // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value + // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type LDAPIdentityProviderUserSearchAttributesSpec struct { + // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // +kubebuilder:validation:MinLength=1 + Username string `json:"username,omitempty"` + + // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // +kubebuilder:validation:MinLength=1 + UniqueID string `json:"uniqueID,omitempty"` +} + +type LDAPIdentityProviderUserSearchSpec struct { + // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + Base string `json:"base,omitempty"` + + // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" + // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Optional. When not specified, the default will act as if the Filter were specified as the value from + // Attributes.Username appended by "={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the LDAP entry which was found as + // the result of the user search. + // +optional + Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. // +kubebuilder:validation:MinLength=1 Host string `json:"host"` + + // TLS contains the connection settings for how to establish the connection to the Host. + TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in the LDAP provider. + UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index bdd1bc95..6ddcebad 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -33,7 +33,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -56,6 +56,22 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. +func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderBindSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -92,6 +108,13 @@ func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(LDAPIdentityProviderTLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch return } @@ -121,6 +144,55 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. +func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderTLSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index e70fb51d..1e54d043 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -51,11 +51,80 @@ spec: spec: description: Spec for configuring the identity provider. properties: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the LDAP server to be allowed + to perform searches and binds to validate a user's credentials during + a user's authentication attempt. + properties: + secretName: + description: SecretName contains the name of a namespace-local + Secret object that provides the username and password for an + LDAP bind user. This account will be used to perform LDAP searches. + The Secret should be of type "kubernetes.io/basic-auth" which + includes "username" and "password" keys. The username value + should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + minLength: 1 + type: string + required: + - secretName + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' minLength: 1 type: string + tls: + description: TLS contains the connection settings for how to establish + the connection to the Host. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle) + to trust when connecting to the LDAP provider. If omitted, a + default set of system roots will be trusted. + type: string + type: object + userSearch: + description: UserSearch contains the configuration for searching for + a user by name in the LDAP provider. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the LDAP entry which was found as the result of + the user search. + properties: + uniqueID: + description: UniqueID specifies the name of the attribute + in the LDAP entry which whose value shall be used to uniquely + identify the user within this LDAP provider after a successful + authentication. E.g. "uidNumber" or "objectGUID". + minLength: 1 + type: string + username: + description: Username specifies the name of attribute in the + LDAP entry which whose value shall become the username of + the user after a successful authentication. This would typically + be the same attribute name used in the user search filter. + E.g. "mail" or "uid" or "userPrincipalName". + minLength: 1 + type: string + type: object + base: + description: Base is the DN that should be used as the search + base when searching for users. E.g. "ou=users,dc=example,dc=com". + minLength: 1 + type: string + filter: + description: Filter is the LDAP search filter which should be + applied when searching for users. The pattern "{}" must occur + in the filter and will be dynamically replaced by the username + for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". + For more information about LDAP filters, see https://ldap.com/ldap-filters. + Optional. When not specified, the default will act as if the + Filter were specified as the value from Attributes.Username + appended by "={}". + type: string + type: object required: - host type: object diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 8edfa8db..f49bf630 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -708,6 +708,23 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec"] +==== LDAPIdentityProviderBindSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec"] @@ -724,6 +741,9 @@ Spec for configuring an LDAP identity provider. |=== | Field | Description | *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. |=== @@ -744,6 +764,60 @@ Status of an LDAP identity provider. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec"] +==== LDAPIdentityProviderTLSSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. If omitted, a default set of system roots will be trusted. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec"] +==== LDAPIdentityProviderUserSearchAttributesSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec"] +==== LDAPIdentityProviderUserSearchSpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcauthorizationconfig"] ==== OIDCAuthorizationConfig diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 4be52014..5e602f31 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -28,11 +28,69 @@ type LDAPIdentityProviderStatus struct { Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` } +type LDAPIdentityProviderTLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. + // If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} + +type LDAPIdentityProviderBindSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be + // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value + // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type LDAPIdentityProviderUserSearchAttributesSpec struct { + // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // +kubebuilder:validation:MinLength=1 + Username string `json:"username,omitempty"` + + // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // +kubebuilder:validation:MinLength=1 + UniqueID string `json:"uniqueID,omitempty"` +} + +type LDAPIdentityProviderUserSearchSpec struct { + // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + Base string `json:"base,omitempty"` + + // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" + // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Optional. When not specified, the default will act as if the Filter were specified as the value from + // Attributes.Username appended by "={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the LDAP entry which was found as + // the result of the user search. + // +optional + Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. // +kubebuilder:validation:MinLength=1 Host string `json:"host"` + + // TLS contains the connection settings for how to establish the connection to the Host. + TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in the LDAP provider. + UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index bdd1bc95..6ddcebad 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -33,7 +33,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -56,6 +56,22 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. +func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderBindSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -92,6 +108,13 @@ func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(LDAPIdentityProviderTLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch return } @@ -121,6 +144,55 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. +func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderTLSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index e70fb51d..1e54d043 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -51,11 +51,80 @@ spec: spec: description: Spec for configuring the identity provider. properties: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the LDAP server to be allowed + to perform searches and binds to validate a user's credentials during + a user's authentication attempt. + properties: + secretName: + description: SecretName contains the name of a namespace-local + Secret object that provides the username and password for an + LDAP bind user. This account will be used to perform LDAP searches. + The Secret should be of type "kubernetes.io/basic-auth" which + includes "username" and "password" keys. The username value + should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + minLength: 1 + type: string + required: + - secretName + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' minLength: 1 type: string + tls: + description: TLS contains the connection settings for how to establish + the connection to the Host. + properties: + certificateAuthorityData: + description: X.509 Certificate Authority (base64-encoded PEM bundle) + to trust when connecting to the LDAP provider. If omitted, a + default set of system roots will be trusted. + type: string + type: object + userSearch: + description: UserSearch contains the configuration for searching for + a user by name in the LDAP provider. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the LDAP entry which was found as the result of + the user search. + properties: + uniqueID: + description: UniqueID specifies the name of the attribute + in the LDAP entry which whose value shall be used to uniquely + identify the user within this LDAP provider after a successful + authentication. E.g. "uidNumber" or "objectGUID". + minLength: 1 + type: string + username: + description: Username specifies the name of attribute in the + LDAP entry which whose value shall become the username of + the user after a successful authentication. This would typically + be the same attribute name used in the user search filter. + E.g. "mail" or "uid" or "userPrincipalName". + minLength: 1 + type: string + type: object + base: + description: Base is the DN that should be used as the search + base when searching for users. E.g. "ou=users,dc=example,dc=com". + minLength: 1 + type: string + filter: + description: Filter is the LDAP search filter which should be + applied when searching for users. The pattern "{}" must occur + in the filter and will be dynamically replaced by the username + for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". + For more information about LDAP filters, see https://ldap.com/ldap-filters. + Optional. When not specified, the default will act as if the + Filter were specified as the value from Attributes.Username + appended by "={}". + type: string + type: object required: - host type: object diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 4be52014..5e602f31 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -28,11 +28,69 @@ type LDAPIdentityProviderStatus struct { Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` } +type LDAPIdentityProviderTLSSpec struct { + // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. + // If omitted, a default set of system roots will be trusted. + // +optional + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` +} + +type LDAPIdentityProviderBindSpec struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be + // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value + // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + SecretName string `json:"secretName"` +} + +type LDAPIdentityProviderUserSearchAttributesSpec struct { + // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // +kubebuilder:validation:MinLength=1 + Username string `json:"username,omitempty"` + + // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // +kubebuilder:validation:MinLength=1 + UniqueID string `json:"uniqueID,omitempty"` +} + +type LDAPIdentityProviderUserSearchSpec struct { + // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // +kubebuilder:validation:MinLength=1 + Base string `json:"base,omitempty"` + + // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" + // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Optional. When not specified, the default will act as if the Filter were specified as the value from + // Attributes.Username appended by "={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the LDAP entry which was found as + // the result of the user search. + // +optional + Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. // +kubebuilder:validation:MinLength=1 Host string `json:"host"` + + // TLS contains the connection settings for how to establish the connection to the Host. + TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in the LDAP provider. + UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index bdd1bc95..6ddcebad 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -33,7 +33,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -56,6 +56,22 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. +func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderBindSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -92,6 +108,13 @@ func (in *LDAPIdentityProviderList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) { *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(LDAPIdentityProviderTLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch return } @@ -121,6 +144,55 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. +func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderTLSSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. +func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributesSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. +func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCAuthorizationConfig) DeepCopyInto(out *OIDCAuthorizationConfig) { *out = *in diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index c52e7776..62c6e09a 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -317,8 +317,7 @@ export PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS='${supervisor_custom_labels}' export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345" export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344" export PINNIPED_TEST_PROXY=http://127.0.0.1:12346 -export PINNIPED_TEST_LDAP_LDAP_URL=ldap://ldap.tools.svc.cluster.local -export PINNIPED_TEST_LDAP_LDAPS_URL=ldaps://ldap.tools.svc.cluster.local +export PINNIPED_TEST_LDAP_HOST=ldap.tools.svc.cluster.local export PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE="${test_ca_bundle_pem}" export PINNIPED_TEST_LDAP_BIND_ACCOUNT_USERNAME="cn=admin,dc=pinniped,dc=dev" export PINNIPED_TEST_LDAP_BIND_ACCOUNT_PASSWORD=password @@ -327,6 +326,8 @@ export PINNIPED_TEST_LDAP_GROUPS_SEARCH_BASE="ou=groups,dc=pinniped,dc=dev" export PINNIPED_TEST_LDAP_USER_DN="cn=pinny,ou=users,dc=pinniped,dc=dev" export PINNIPED_TEST_LDAP_USER_CN="pinny" export PINNIPED_TEST_LDAP_USER_PASSWORD=${ldap_test_password} +export PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_NAME="uidNumber" +export PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_VALUE="1000" export PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME="mail" export PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE="pinny.ldap@example.com" export PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_DN="cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev;cn=seals,ou=groups,dc=pinniped,dc=dev" diff --git a/test/deploy/tools/ldap.yaml b/test/deploy/tools/ldap.yaml index 9e178407..20040599 100644 --- a/test/deploy/tools/ldap.yaml +++ b/test/deploy/tools/ldap.yaml @@ -140,9 +140,7 @@ spec: spec: containers: - name: ldap - #! An issue was reported and will be fixed in bitnami/openldap soon. - image: ghcr.io/pinniped-ci-bot/bitnami-openldap-forked:2.4.58-debian-10-r15 #! our own fork of docker.io/bitnami/openldap - #! image: docker.io/bitnami/openldap + image: docker.io/bitnami/openldap imagePullPolicy: Always ports: - name: ldap @@ -182,9 +180,6 @@ spec: value: "/var/certs/ldap-key.pem" - name: LDAP_TLS_CA_FILE value: "/var/certs/ca.pem" - #! This env var was added in our fork to reduce slapd memory consumption from ~700 MB to ~12 MB. - - name: LDAP_ULIMIT_MAX_FILES - value: "1024" #! Note that the custom LDIF file is only read at pod start-up time. - name: LDAP_CUSTOM_LDIF_DIR value: "/var/ldifs" diff --git a/test/integration/cli_test.go b/test/integration/cli_test.go index 1221a604..149b9194 100644 --- a/test/integration/cli_test.go +++ b/test/integration/cli_test.go @@ -190,9 +190,9 @@ func TestCLILoginOIDC(t *testing.T) { require.NoError(t, err) claims := map[string]interface{}{} require.NoError(t, json.Unmarshal(jws.UnsafePayloadWithoutVerification(), &claims)) - require.Equal(t, env.CLITestUpstream.Issuer, claims["iss"]) - require.Equal(t, env.CLITestUpstream.ClientID, claims["aud"]) - require.Equal(t, env.CLITestUpstream.Username, claims["email"]) + require.Equal(t, env.CLIUpstreamOIDC.Issuer, claims["iss"]) + require.Equal(t, env.CLIUpstreamOIDC.ClientID, claims["aud"]) + require.Equal(t, env.CLIUpstreamOIDC.Username, claims["email"]) require.NotEmpty(t, claims["nonce"]) // Run the CLI again with the same session cache and login parameters. @@ -211,10 +211,10 @@ func TestCLILoginOIDC(t *testing.T) { t.Logf("overwriting cache to remove valid ID token") cache := filesession.New(sessionCachePath) cacheKey := oidcclient.SessionCacheKey{ - Issuer: env.CLITestUpstream.Issuer, - ClientID: env.CLITestUpstream.ClientID, + Issuer: env.CLIUpstreamOIDC.Issuer, + ClientID: env.CLIUpstreamOIDC.ClientID, Scopes: []string{"email", "offline_access", "openid", "profile"}, - RedirectURI: strings.ReplaceAll(env.CLITestUpstream.CallbackURL, "127.0.0.1", "localhost"), + RedirectURI: strings.ReplaceAll(env.CLIUpstreamOIDC.CallbackURL, "127.0.0.1", "localhost"), } cached := cache.GetToken(cacheKey) require.NotNil(t, cached) @@ -325,11 +325,11 @@ func runPinnipedLoginOIDC( require.NoError(t, page.Navigate(loginURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.CLITestUpstream) + browsertest.LoginToUpstream(t, page, env.CLIUpstreamOIDC) // Expect to be redirected to the localhost callback. t.Logf("waiting for redirect to callback") - callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLITestUpstream.CallbackURL) + `\?.+\z`) + callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLIUpstreamOIDC.CallbackURL) + `\?.+\z`) browsertest.WaitForURL(t, page, callbackURLPattern) // Wait for the "pre" element that gets rendered for a `text/plain` page, and @@ -375,11 +375,11 @@ func spawnTestGoroutine(t *testing.T, f func() error) { func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, sessionCachePath string) *exec.Cmd { env := library.IntegrationEnv(t) - callbackURL, err := url.Parse(env.CLITestUpstream.CallbackURL) + callbackURL, err := url.Parse(env.CLIUpstreamOIDC.CallbackURL) require.NoError(t, err) cmd := exec.CommandContext(ctx, pinnipedExe, "login", "oidc", - "--issuer", env.CLITestUpstream.Issuer, - "--client-id", env.CLITestUpstream.ClientID, + "--issuer", env.CLIUpstreamOIDC.Issuer, + "--client-id", env.CLIUpstreamOIDC.ClientID, "--scopes", "offline_access,openid,email,profile", "--listen-port", callbackURL.Port(), "--session-cache", sessionCachePath, @@ -387,9 +387,9 @@ func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, ses ) // If there is a custom CA bundle, pass it via --ca-bundle and a temporary file. - if env.CLITestUpstream.CABundle != "" { + if env.CLIUpstreamOIDC.CABundle != "" { path := filepath.Join(testutil.TempDir(t), "test-ca.pem") - require.NoError(t, ioutil.WriteFile(path, []byte(env.CLITestUpstream.CABundle), 0600)) + require.NoError(t, ioutil.WriteFile(path, []byte(env.CLIUpstreamOIDC.CABundle), 0600)) cmd.Args = append(cmd.Args, "--ca-bundle", path) } diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 2cb207e7..3ab00ffb 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -53,7 +53,7 @@ func TestE2EFullIntegration(t *testing.T) { page := browsertest.Open(t) // Infer the downstream issuer URL from the callback associated with the upstream test client registration. - issuerURL, err := url.Parse(env.SupervisorTestUpstream.CallbackURL) + issuerURL, err := url.Parse(env.SupervisorUpstreamOIDC.CallbackURL) require.NoError(t, err) require.True(t, strings.HasSuffix(issuerURL.Path, "/callback")) issuerURL.Path = strings.TrimSuffix(issuerURL.Path, "/callback") @@ -66,7 +66,7 @@ func TestE2EFullIntegration(t *testing.T) { // Save that bundle plus the one that signs the upstream issuer, for test purposes. testCABundlePath := filepath.Join(tempDir, "test-ca.pem") - testCABundlePEM := []byte(string(ca.Bundle()) + "\n" + env.SupervisorTestUpstream.CABundle) + testCABundlePEM := []byte(string(ca.Bundle()) + "\n" + env.SupervisorUpstreamOIDC.CABundle) testCABundleBase64 := base64.StdEncoding.EncodeToString(testCABundlePEM) require.NoError(t, ioutil.WriteFile(testCABundlePath, testCABundlePEM, 0600)) @@ -94,19 +94,19 @@ func TestE2EFullIntegration(t *testing.T) { // Create upstream OIDC provider and wait for it to become ready. library.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorTestUpstream.Issuer, + Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)), + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), }, AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: env.SupervisorTestUpstream.AdditionalScopes, + AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, }, Claims: idpv1alpha1.OIDCClaims{ - Username: env.SupervisorTestUpstream.UsernameClaim, - Groups: env.SupervisorTestUpstream.GroupsClaim, + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, }, Client: idpv1alpha1.OIDCClient{ - SecretName: library.CreateClientCredsSecret(t, env.SupervisorTestUpstream.ClientID, env.SupervisorTestUpstream.ClientSecret).Name, + SecretName: library.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) @@ -120,10 +120,10 @@ func TestE2EFullIntegration(t *testing.T) { // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. library.CreateTestClusterRoleBinding(t, - rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: env.SupervisorTestUpstream.Username}, + rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: env.SupervisorUpstreamOIDC.Username}, rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"}, ) - library.WaitForUserToHaveAccess(t, env.SupervisorTestUpstream.Username, []string{}, &authorizationv1.ResourceAttributes{ + library.WaitForUserToHaveAccess(t, env.SupervisorUpstreamOIDC.Username, []string{}, &authorizationv1.ResourceAttributes{ Verb: "get", Group: "", Version: "v1", @@ -240,7 +240,7 @@ func TestE2EFullIntegration(t *testing.T) { require.NoError(t, page.Navigate(loginURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorTestUpstream) + browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) // Expect to be redirected to the localhost callback. t.Logf("waiting for redirect to callback") @@ -290,11 +290,11 @@ func TestE2EFullIntegration(t *testing.T) { require.NotNil(t, token) idTokenClaims := token.IDToken.Claims - require.Equal(t, env.SupervisorTestUpstream.Username, idTokenClaims[oidc.DownstreamUsernameClaim]) + require.Equal(t, env.SupervisorUpstreamOIDC.Username, idTokenClaims[oidc.DownstreamUsernameClaim]) // The groups claim in the file ends up as an []interface{}, so adjust our expectation to match. - expectedGroups := make([]interface{}, 0, len(env.SupervisorTestUpstream.ExpectedGroups)) - for _, g := range env.SupervisorTestUpstream.ExpectedGroups { + expectedGroups := make([]interface{}, 0, len(env.SupervisorUpstreamOIDC.ExpectedGroups)) + for _, g := range env.SupervisorUpstreamOIDC.ExpectedGroups { expectedGroups = append(expectedGroups, g) } require.Equal(t, expectedGroups, idTokenClaims[oidc.DownstreamGroupsClaim]) @@ -302,7 +302,7 @@ func TestE2EFullIntegration(t *testing.T) { // confirm we are the right user according to Kube expectedYAMLGroups := func() string { var b strings.Builder - for _, g := range env.SupervisorTestUpstream.ExpectedGroups { + for _, g := range env.SupervisorUpstreamOIDC.ExpectedGroups { b.WriteString("\n") b.WriteString(` - `) b.WriteString(g) @@ -328,7 +328,7 @@ status: user: groups:`+expectedYAMLGroups+` - system:authenticated - username: `+env.SupervisorTestUpstream.Username+` + username: `+env.SupervisorUpstreamOIDC.Username+` `, string(kubectlOutput3)) @@ -339,7 +339,7 @@ status: true, pinnipedExe, kubeconfigPath, - env.SupervisorTestUpstream.Username, - append(env.SupervisorTestUpstream.ExpectedGroups, "system:authenticated"), + env.SupervisorUpstreamOIDC.Username, + append(env.SupervisorUpstreamOIDC.ExpectedGroups, "system:authenticated"), ) } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 3af75826..909d6b1b 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "net/url" @@ -36,43 +37,89 @@ import ( ) func TestSupervisorLogin(t *testing.T) { + env := library.IntegrationEnv(t) + tests := []struct { - name string - createIDP func(t *testing.T) - requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string) + name string + createIDP func(t *testing.T) + requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client) + wantDownstreamIDTokenSubjectToMatch string + wantDownstreamIDTokenUsernameToMatch string }{ { name: "oidc", createIDP: func(t *testing.T) { t.Helper() - env := library.IntegrationEnv(t) library.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorTestUpstream.Issuer, + Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)), + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), }, Client: idpv1alpha1.OIDCClient{ - SecretName: library.CreateClientCredsSecret(t, env.SupervisorTestUpstream.ClientID, env.SupervisorTestUpstream.ClientSecret).Name, + SecretName: library.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, }, }, idpv1alpha1.PhaseReady) }, requestAuthorization: requestAuthorizationUsingOIDCIdentityProvider, + // the ID token Subject should include the upstream user ID after the upstream issuer name + wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + // the ID token Username should include the upstream user ID after the upstream issuer name + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", }, + // TODO add more variations of this LDAP test to try using different user search filters and attributes { name: "ldap", createIDP: func(t *testing.T) { t.Helper() + secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, + }, + ) library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ - Host: "something", - }, "") // TODO: this should be Ready! + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.LDAPIdentityProviderTLSSpec{ + CertificateAuthorityData: env.SupervisorUpstreamLDAP.CABundle, + }, + Bind: idpv1alpha1.LDAPIdentityProviderBindSpec{ + SecretName: secret.Name, + }, + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearchSpec{ + Base: env.SupervisorUpstreamLDAP.UserSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ + Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + UniqueID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + }, + }, + }, "") // TODO: this should be idpv1alpha1.LDAPPhaseReady once we have a controller }, - requestAuthorization: requestAuthorizationUsingLDAPIdentityProvider, + requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { + requestAuthorizationUsingLDAPIdentityProvider(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login + httpClient, + ) + }, + // 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 + "?sub=" + env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue, + ), + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - testSupervisorLogin(t, test.createIDP, test.requestAuthorization) + testSupervisorLogin(t, + test.createIDP, + test.requestAuthorization, + test.wantDownstreamIDTokenSubjectToMatch, + test.wantDownstreamIDTokenUsernameToMatch, + ) }) } } @@ -80,7 +127,8 @@ func TestSupervisorLogin(t *testing.T) { func testSupervisorLogin( t *testing.T, createIDP func(t *testing.T), - requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string), + requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client), + wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, ) { env := library.IntegrationEnv(t) @@ -88,7 +136,7 @@ func testSupervisorLogin( defer cancel() // Infer the downstream issuer URL from the callback associated with the upstream test client registration. - issuerURL, err := url.Parse(env.SupervisorTestUpstream.CallbackURL) + issuerURL, err := url.Parse(env.SupervisorUpstreamOIDC.CallbackURL) require.NoError(t, err) require.True(t, strings.HasSuffix(issuerURL.Path, "/callback")) issuerURL.Path = strings.TrimSuffix(issuerURL.Path, "/callback") @@ -201,7 +249,7 @@ func testSupervisorLogin( pkceParam.Method(), ) - // Make the authorize request one "manually" so we can check its response headers. + // Make the authorize request once "manually" so we can check its response security headers. authorizeRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) require.NoError(t, err) authorizeResp, err := httpClient.Do(authorizeRequest) @@ -210,7 +258,7 @@ func testSupervisorLogin( expectSecurityHeaders(t, authorizeResp) // Perform parameterized auth code acquisition. - requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL) + requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, httpClient) // Expect that our callback handler was invoked. callback := localCallbackServer.waitForCallback(10 * time.Second) @@ -225,7 +273,9 @@ func testSupervisorLogin( require.NoError(t, err) expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"} - verifyTokenResponse(t, tokenResponse, discovery, downstreamOAuth2Config, env.SupervisorTestUpstream.Issuer, nonceParam, expectedIDTokenClaims) + verifyTokenResponse(t, + tokenResponse, discovery, downstreamOAuth2Config, nonceParam, + expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch) // token exchange on the original token doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery) @@ -236,7 +286,9 @@ func testSupervisorLogin( require.NoError(t, err) expectedIDTokenClaims = append(expectedIDTokenClaims, "at_hash") - verifyTokenResponse(t, refreshedTokenResponse, discovery, downstreamOAuth2Config, env.SupervisorTestUpstream.Issuer, "", expectedIDTokenClaims) + verifyTokenResponse(t, + refreshedTokenResponse, discovery, downstreamOAuth2Config, "", + expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch) require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken) require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken) @@ -251,9 +303,9 @@ func verifyTokenResponse( tokenResponse *oauth2.Token, discovery *coreosoidc.Provider, downstreamOAuth2Config oauth2.Config, - upstreamIssuerName string, nonceParam nonce.Nonce, expectedIDTokenClaims []string, + wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, ) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -265,14 +317,17 @@ func verifyTokenResponse( idToken, err := verifier.Verify(ctx, rawIDToken) require.NoError(t, err) - // Check the claims of the ID token. - expectedSubjectPrefix := upstreamIssuerName + "?sub=" - require.True(t, strings.HasPrefix(idToken.Subject, expectedSubjectPrefix)) - require.Greater(t, len(idToken.Subject), len(expectedSubjectPrefix), - "the ID token Subject should include the upstream user ID after the upstream issuer name") + // Check the sub claim of the ID token. + require.Regexp(t, wantDownstreamIDTokenSubjectToMatch, idToken.Subject) + + // Check the nonce claim of the ID token. require.NoError(t, nonceParam.Validate(idToken)) + + // Check the exp claim of the ID token. expectedIDTokenLifetime := oidc.DefaultOIDCTimeoutsConfiguration().IDTokenLifespan testutil.RequireTimeInDelta(t, time.Now().UTC().Add(expectedIDTokenLifetime), idToken.Expiry, time.Second*30) + + // Check the full list of claim names of the ID token. idTokenClaims := map[string]interface{}{} err = idToken.Claims(&idTokenClaims) require.NoError(t, err) @@ -281,10 +336,9 @@ func verifyTokenResponse( idTokenClaimNames = append(idTokenClaimNames, k) } require.ElementsMatch(t, expectedIDTokenClaims, idTokenClaimNames) - expectedUsernamePrefix := upstreamIssuerName + "?sub=" - require.True(t, strings.HasPrefix(idTokenClaims["username"].(string), expectedUsernamePrefix)) - require.Greater(t, len(idTokenClaims["username"].(string)), len(expectedUsernamePrefix), - "the ID token Username should include the upstream user ID after the upstream issuer name") + + // Check username claim of the ID token. + require.Regexp(t, wantDownstreamIDTokenUsernameToMatch, idTokenClaims["username"].(string)) // Some light verification of the other tokens that were returned. require.NotEmpty(t, tokenResponse.AccessToken) @@ -296,7 +350,7 @@ func verifyTokenResponse( require.NotEmpty(t, tokenResponse.RefreshToken) } -func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string) { +func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, _ *http.Client) { t.Helper() env := library.IntegrationEnv(t) @@ -306,7 +360,7 @@ func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAutho require.NoError(t, page.Navigate(downstreamAuthorizeURL)) // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorTestUpstream) + browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) // Wait for the login to happen and us be redirected back to a localhost callback. t.Logf("waiting for redirect to callback") @@ -314,9 +368,29 @@ func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAutho browsertest.WaitForURL(t, page, callbackURLPattern) } -func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string) { +func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAuthorizeURL, upstreamUsername, upstreamPassword string, httpClient *http.Client) { t.Helper() - t.Skip("implement me!") + + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) + defer cancelFunc() + + authRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) + require.NoError(t, err) + + // Set the custom username/password headers for the LDAP authorize request. + authRequest.Header.Set("X-Pinniped-Upstream-Username", upstreamUsername) + authRequest.Header.Set("X-Pinniped-Upstream-Password", upstreamPassword) + + // The authorize request is supposed to redirect to this test's callback handler, which in turn is supposed to return 200 OK. + authResponse, err := httpClient.Do(authRequest) + require.NoError(t, err) + responseBody, err := ioutil.ReadAll(authResponse.Body) + defer authResponse.Body.Close() + require.NoError(t, err) + + t.Skip("The rest of this test will not work until we implement the corresponding production code.") // TODO remove this skip + + require.Equalf(t, http.StatusOK, authResponse.StatusCode, "response body was: %s", string(responseBody)) } func startLocalCallbackServer(t *testing.T) *localCallbackServer { diff --git a/test/integration/supervisor_upstream_test.go b/test/integration/supervisor_upstream_test.go index b43735a6..5b4c0cfd 100644 --- a/test/integration/supervisor_upstream_test.go +++ b/test/integration/supervisor_upstream_test.go @@ -45,9 +45,9 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) { t.Run("valid", func(t *testing.T) { t.Parallel() spec := v1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorTestUpstream.Issuer, + Issuer: env.SupervisorUpstreamOIDC.Issuer, TLS: &v1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)), + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), }, AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ AdditionalScopes: []string{"email", "profile"}, diff --git a/test/library/client.go b/test/library/client.go index acbf7626..ef242613 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -190,13 +190,13 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty // test's lifetime. It returns a corev1.TypedLocalObjectReference which describes the test JWT // authenticator within the test namespace. // -// CreateTestJWTAuthenticatorForCLIUpstream gets the OIDC issuer info from IntegrationEnv().CLITestUpstream. +// CreateTestJWTAuthenticatorForCLIUpstream gets the OIDC issuer info from IntegrationEnv().CLIUpstreamOIDC. func CreateTestJWTAuthenticatorForCLIUpstream(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference { t.Helper() testEnv := IntegrationEnv(t) spec := auth1alpha1.JWTAuthenticatorSpec{ - Issuer: testEnv.CLITestUpstream.Issuer, - Audience: testEnv.CLITestUpstream.ClientID, + Issuer: testEnv.CLIUpstreamOIDC.Issuer, + Audience: testEnv.CLIUpstreamOIDC.ClientID, // The default UsernameClaim is "username" but the upstreams that we use for // integration tests won't necessarily have that claim, so use "sub" here. Claims: auth1alpha1.JWTTokenClaims{Username: "sub"}, @@ -204,9 +204,9 @@ func CreateTestJWTAuthenticatorForCLIUpstream(ctx context.Context, t *testing.T) // If the test upstream does not have a CA bundle specified, then don't configure one in the // JWTAuthenticator. Leaving TLSSpec set to nil will result in OIDC discovery using the OS's root // CA store. - if testEnv.CLITestUpstream.CABundle != "" { + if testEnv.CLIUpstreamOIDC.CABundle != "" { spec.TLS = &auth1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(testEnv.CLITestUpstream.CABundle)), + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(testEnv.CLIUpstreamOIDC.CABundle)), } } return CreateTestJWTAuthenticator(ctx, t, spec) @@ -250,8 +250,7 @@ func CreateTestJWTAuthenticator(ctx context.Context, t *testing.T, spec auth1alp // CreateTestFederationDomain creates and returns a test FederationDomain in // $PINNIPED_TEST_SUPERVISOR_NAMESPACE, which will be automatically deleted at the end of the -// current test's lifetime. It generates a random, valid, issuer for the FederationDomain. -// +// current test's lifetime. // If the provided issuer is not the empty string, then it will be used for the // FederationDomain.Spec.Issuer field. Else, a random issuer will be generated. func CreateTestFederationDomain(ctx context.Context, t *testing.T, issuer string, certSecretName string, expectStatus configv1alpha1.FederationDomainStatusCondition) *configv1alpha1.FederationDomain { diff --git a/test/library/env.go b/test/library/env.go index a19aaa2e..6ecc699f 100644 --- a/test/library/env.go +++ b/test/library/env.go @@ -49,8 +49,9 @@ type TestEnv struct { ExpectedGroups []string `json:"expectedGroups"` } `json:"testUser"` - CLITestUpstream TestOIDCUpstream `json:"cliOIDCUpstream"` - SupervisorTestUpstream TestOIDCUpstream `json:"supervisorOIDCUpstream"` + CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"` + SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"` + SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"` } type TestOIDCUpstream struct { @@ -67,6 +68,21 @@ type TestOIDCUpstream struct { ExpectedGroups []string `json:"expectedGroups"` } +type TestLDAPUpstream struct { + Host string `json:"host"` + CABundle string `json:"caBundle"` + BindUsername string `json:"bindUsername"` + BindPassword string `json:"bindPassword"` + UserSearchBase string `json:"userSearchBase"` + TestUserDN string `json:"testUserDN"` + TestUserCN string `json:"testUserCN"` + TestUserPassword string `json:"testUserPassword"` + TestUserMailAttributeName string `json:"testUserMailAttributeName"` + TestUserMailAttributeValue string `json:"testUserMailAttributeValue"` + TestUserUniqueIDAttributeName string `json:"testUserUniqueIDAttributeName"` + TestUserUniqueIDAttributeValue string `json:"testUserUniqueIDAttributeValue"` +} + // ProxyEnv returns a set of environment variable strings (e.g., to combine with os.Environ()) which set up the configured test HTTP proxy. func (e *TestEnv) ProxyEnv() []string { if e.Proxy == "" { @@ -180,7 +196,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { result.Proxy = os.Getenv("PINNIPED_TEST_PROXY") result.APIGroupSuffix = wantEnv("PINNIPED_TEST_API_GROUP_SUFFIX", "pinniped.dev") - result.CLITestUpstream = TestOIDCUpstream{ + result.CLIUpstreamOIDC = TestOIDCUpstream{ Issuer: needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER"), CABundle: os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE"), ClientID: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID"), @@ -189,7 +205,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { Password: needEnv(t, "PINNIPED_TEST_CLI_OIDC_PASSWORD"), } - result.SupervisorTestUpstream = TestOIDCUpstream{ + result.SupervisorUpstreamOIDC = TestOIDCUpstream{ Issuer: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER"), CABundle: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE"), AdditionalScopes: strings.Fields(os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES")), @@ -202,6 +218,21 @@ func loadEnvVars(t *testing.T, result *TestEnv) { Password: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD"), ExpectedGroups: filterEmpty(strings.Split(strings.ReplaceAll(os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_EXPECTED_GROUPS"), " ", ""), ",")), } + + result.SupervisorUpstreamLDAP = TestLDAPUpstream{ + Host: needEnv(t, "PINNIPED_TEST_LDAP_HOST"), + CABundle: needEnv(t, "PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE"), + BindUsername: needEnv(t, "PINNIPED_TEST_LDAP_BIND_ACCOUNT_USERNAME"), + BindPassword: needEnv(t, "PINNIPED_TEST_LDAP_BIND_ACCOUNT_PASSWORD"), + UserSearchBase: needEnv(t, "PINNIPED_TEST_LDAP_USERS_SEARCH_BASE"), + TestUserDN: needEnv(t, "PINNIPED_TEST_LDAP_USER_DN"), + TestUserCN: needEnv(t, "PINNIPED_TEST_LDAP_USER_CN"), + TestUserUniqueIDAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_NAME"), + TestUserUniqueIDAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_VALUE"), + TestUserMailAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME"), + TestUserMailAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE"), + TestUserPassword: needEnv(t, "PINNIPED_TEST_LDAP_USER_PASSWORD"), + } } func (e *TestEnv) HasCapability(cap Capability) bool { From 1f5978aa1a6c77d77e15fa34b8c92e6909c53c8a Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 7 Apr 2021 16:12:13 -0700 Subject: [PATCH 04/59] Supervisor pre-factor to make room for upstream LDAP identity providers --- .../upstreamwatcher/upstreamwatcher.go | 4 +- .../upstreamwatcher/upstreamwatcher_test.go | 6 +- internal/ldap/ldap.go | 20 ++++++ internal/oidc/auth/auth_handler.go | 10 +-- internal/oidc/auth/auth_handler_test.go | 70 +++++++++---------- internal/oidc/callback/callback_handler.go | 8 +-- .../oidc/callback/callback_handler_test.go | 4 +- internal/oidc/oidc.go | 15 +++- internal/oidc/oidctestutil/oidc.go | 25 +++++-- .../provider/dynamic_upstream_idp_provider.go | 45 +++++++++--- internal/oidc/provider/manager/manager.go | 28 ++++---- .../oidc/provider/manager/manager_test.go | 8 +-- test/integration/supervisor_login_test.go | 4 +- 13 files changed, 157 insertions(+), 90 deletions(-) create mode 100644 internal/ldap/ldap.go diff --git a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go index 345d2b5f..a4cc821e 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go @@ -66,7 +66,7 @@ const ( // IDPCache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. type IDPCache interface { - SetIDPList([]provider.UpstreamOIDCIdentityProviderI) + SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI) } // lruValidatorCache caches the *oidc.Provider associated with a particular issuer/TLS configuration. @@ -159,7 +159,7 @@ func (c *controller) Sync(ctx controllerlib.Context) error { validatedUpstreams = append(validatedUpstreams, provider.UpstreamOIDCIdentityProviderI(valid)) } } - c.cache.SetIDPList(validatedUpstreams) + c.cache.SetOIDCIdentityProviders(validatedUpstreams) if requeue { return controllerlib.ErrSyntheticRequeue } diff --git a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go index f7397352..0b199a11 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go @@ -76,7 +76,7 @@ func TestControllerFilterSecret(t *testing.T) { kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) testLog := testlogger.New(t) cache := provider.NewDynamicUpstreamIDPProvider() - cache.SetIDPList([]provider.UpstreamOIDCIdentityProviderI{ + cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) secretInformer := kubeInformers.Core().V1().Secrets() @@ -611,7 +611,7 @@ func TestController(t *testing.T) { kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) testLog := testlogger.New(t) cache := provider.NewDynamicUpstreamIDPProvider() - cache.SetIDPList([]provider.UpstreamOIDCIdentityProviderI{ + cache.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) @@ -640,7 +640,7 @@ func TestController(t *testing.T) { } require.Equal(t, strings.Join(tt.wantLogs, "\n"), strings.Join(testLog.Lines(), "\n")) - actualIDPList := cache.GetIDPList() + actualIDPList := cache.GetOIDCIdentityProviders() require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) for i := range actualIDPList { actualIDP := actualIDPList[i].(*upstreamoidc.ProviderConfig) diff --git a/internal/ldap/ldap.go b/internal/ldap/ldap.go new file mode 100644 index 00000000..182971d4 --- /dev/null +++ b/internal/ldap/ldap.go @@ -0,0 +1,20 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package ldap contains common LDAP functionality needed by Pinniped. +package ldap + +import ( + "context" + + "k8s.io/apiserver/pkg/authentication/authenticator" +) + +// This interface is similar to the k8s token authenticator, but works with username/passwords instead +// of a single token string. +// +// 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) +} diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index e5d359a8..394d8432 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package auth provides a handler for the OIDC authorization endpoint. @@ -27,7 +27,7 @@ import ( func NewHandler( downstreamIssuer string, - idpListGetter oidc.IDPListGetter, + idpLister oidc.UpstreamIdentityProvidersLister, oauthHelper fosite.OAuth2Provider, generateCSRF func() (csrftoken.CSRFToken, error), generatePKCE func() (pkce.Code, error), @@ -52,7 +52,7 @@ func NewHandler( return nil } - upstreamIDP, err := chooseUpstreamIDP(idpListGetter) + upstreamIDP, err := chooseUpstreamIDP(idpLister) if err != nil { plog.WarningErr("authorize upstream config", err) return err @@ -165,8 +165,8 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { return csrfFromCookie } -func chooseUpstreamIDP(idpListGetter oidc.IDPListGetter) (provider.UpstreamOIDCIdentityProviderI, error) { - allUpstreamIDPs := idpListGetter.GetIDPList() +func chooseUpstreamIDP(idpLister oidc.UpstreamOIDCIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, error) { + allUpstreamIDPs := idpLister.GetOIDCIdentityProviders() if len(allUpstreamIDPs) == 0 { return nil, httperr.New( http.StatusUnprocessableEntity, diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index a0f715d1..a69fa3c4 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package auth @@ -244,7 +244,7 @@ func TestAuthorizationEndpoint(t *testing.T) { name string issuer string - idpListGetter provider.DynamicUpstreamIDPProvider + idpLister provider.DynamicUpstreamIDPProvider generateCSRF func() (csrftoken.CSRFToken, error) generatePKCE func() (pkce.Code, error) generateNonce func() (nonce.Nonce, error) @@ -270,7 +270,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "happy path using GET without a CSRF cookie", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -288,7 +288,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "happy path using GET with a CSRF cookie", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -306,7 +306,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "happy path using POST", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -326,7 +326,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "happy path with prompt param login passed through to redirect uri", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -346,7 +346,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "error while decoding CSRF cookie just generates a new cookie and succeeds as usual", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -366,7 +366,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "happy path when downstream redirect uri matches what is configured for client except for the port number", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -388,7 +388,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "happy path when downstream requested scopes include offline_access", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -408,7 +408,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "downstream redirect uri does not match what is configured for client", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -425,7 +425,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "downstream client does not exist", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -440,7 +440,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "response type is unsupported", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -456,7 +456,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "downstream scopes do not match what is configured for client", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -472,7 +472,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "missing response type in request", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -488,7 +488,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "missing client id in request", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -503,7 +503,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "missing PKCE code_challenge in request", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -519,7 +519,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "invalid value for PKCE code_challenge_method in request", // https://tools.ietf.org/html/rfc7636#section-4.3 issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -535,7 +535,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "when PKCE code_challenge_method in request is `plain`", // https://tools.ietf.org/html/rfc7636#section-4.3 issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -551,7 +551,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "missing PKCE code_challenge_method in request", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -569,7 +569,7 @@ func TestAuthorizationEndpoint(t *testing.T) { // through that part of the fosite library. name: "prompt param is not allowed to have none and another legal value at the same time", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -585,7 +585,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "OIDC validations are skipped when the openid scope was not requested", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -606,7 +606,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "state does not have enough entropy", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -622,7 +622,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "error while encoding upstream state param", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -637,7 +637,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "error while encoding CSRF cookie value for new cookie", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -652,7 +652,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "error while generating CSRF token", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: func() (csrftoken.CSRFToken, error) { return "", fmt.Errorf("some csrf generator error") }, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, @@ -667,7 +667,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "error while generating nonce", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: func() (nonce.Nonce, error) { return "", fmt.Errorf("some nonce generator error") }, @@ -682,7 +682,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "error while generating PKCE", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: func() (pkce.Code, error) { return "", fmt.Errorf("some PKCE generator error") }, generateNonce: happyNonceGenerator, @@ -697,7 +697,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "no upstream providers are configured", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(), // empty + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC().Build(), // empty method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -707,7 +707,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "too many upstream providers are configured", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider, &upstreamOIDCIdentityProvider), // more than one not allowed + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider, &upstreamOIDCIdentityProvider).Build(), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusUnprocessableEntity, @@ -717,7 +717,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "PUT is a bad method", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), method: http.MethodPut, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -727,7 +727,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "PATCH is a bad method", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), method: http.MethodPatch, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -737,7 +737,7 @@ func TestAuthorizationEndpoint(t *testing.T) { { name: "DELETE is a bad method", issuer: downstreamIssuer, - idpListGetter: oidctestutil.NewIDPListGetter(&upstreamOIDCIdentityProvider), + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), method: http.MethodDelete, path: "/some/path", wantStatus: http.StatusMethodNotAllowed, @@ -803,7 +803,7 @@ func TestAuthorizationEndpoint(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - subject := NewHandler(test.issuer, test.idpListGetter, oauthHelper, test.generateCSRF, test.generatePKCE, test.generateNonce, test.stateEncoder, test.cookieEncoder) + subject := NewHandler(test.issuer, test.idpLister, oauthHelper, test.generateCSRF, test.generatePKCE, test.generateNonce, test.stateEncoder, test.cookieEncoder) runOneTestCase(t, test, subject) }) } @@ -812,7 +812,7 @@ func TestAuthorizationEndpoint(t *testing.T) { test := tests[0] require.Equal(t, "happy path using GET without a CSRF cookie", test.name) // re-use the happy path test case - subject := NewHandler(test.issuer, test.idpListGetter, oauthHelper, test.generateCSRF, test.generatePKCE, test.generateNonce, test.stateEncoder, test.cookieEncoder) + subject := NewHandler(test.issuer, test.idpLister, oauthHelper, test.generateCSRF, test.generatePKCE, test.generateNonce, test.stateEncoder, test.cookieEncoder) runOneTestCase(t, test, subject) @@ -823,7 +823,7 @@ func TestAuthorizationEndpoint(t *testing.T) { AuthorizationURL: *upstreamAuthURL, Scopes: []string{"other-scope1", "other-scope2"}, } - test.idpListGetter.SetIDPList([]provider.UpstreamOIDCIdentityProviderI{provider.UpstreamOIDCIdentityProviderI(&newProviderSettings)}) + test.idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{provider.UpstreamOIDCIdentityProviderI(&newProviderSettings)}) // Update the expectations of the test case to match the new upstream IDP settings. test.wantLocationHeader = urlWithQuery(upstreamAuthURL.String(), diff --git a/internal/oidc/callback/callback_handler.go b/internal/oidc/callback/callback_handler.go index a103824e..5bece1d9 100644 --- a/internal/oidc/callback/callback_handler.go +++ b/internal/oidc/callback/callback_handler.go @@ -33,7 +33,7 @@ const ( ) func NewHandler( - idpListGetter oidc.IDPListGetter, + upstreamIDPs oidc.UpstreamOIDCIdentityProvidersLister, oauthHelper fosite.OAuth2Provider, stateDecoder, cookieDecoder oidc.Decoder, redirectURI string, @@ -44,7 +44,7 @@ func NewHandler( return err } - upstreamIDPConfig := findUpstreamIDPConfig(state.UpstreamName, idpListGetter) + upstreamIDPConfig := findUpstreamIDPConfig(state.UpstreamName, upstreamIDPs) if upstreamIDPConfig == nil { plog.Warning("upstream provider not found") return httperr.New(http.StatusUnprocessableEntity, "upstream provider not found") @@ -143,8 +143,8 @@ func validateRequest(r *http.Request, stateDecoder, cookieDecoder oidc.Decoder) return state, nil } -func findUpstreamIDPConfig(upstreamName string, idpListGetter oidc.IDPListGetter) provider.UpstreamOIDCIdentityProviderI { - for _, p := range idpListGetter.GetIDPList() { +func findUpstreamIDPConfig(upstreamName string, upstreamIDPs oidc.UpstreamOIDCIdentityProvidersLister) provider.UpstreamOIDCIdentityProviderI { + for _, p := range upstreamIDPs.GetOIDCIdentityProviders() { if p.GetName() == upstreamName { return p } diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index 218b6753..ad82928b 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -626,8 +626,8 @@ func TestCallbackEndpoint(t *testing.T) { jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration) - idpListGetter := oidctestutil.NewIDPListGetter(&test.idp) - subject := NewHandler(idpListGetter, oauthHelper, happyStateCodec, happyCookieCodec, happyUpstreamRedirectURI) + idpLister := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&test.idp).Build() + subject := NewHandler(idpLister, oauthHelper, happyStateCodec, happyCookieCodec, happyUpstreamRedirectURI) req := httptest.NewRequest(test.method, test.path, nil) if test.csrfCookie != "" { req.Header.Set("Cookie", test.csrfCookie) diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index da511515..0297b43c 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package oidc contains common OIDC functionality needed by Pinniped. @@ -274,8 +274,17 @@ func FositeErrorForLog(err error) []interface{} { return keysAndValues } -type IDPListGetter interface { - GetIDPList() []provider.UpstreamOIDCIdentityProviderI +type UpstreamOIDCIdentityProvidersLister interface { + GetOIDCIdentityProviders() []provider.UpstreamOIDCIdentityProviderI +} + +type UpstreamLDAPIdentityProvidersLister interface { + GetLDAPIdentityProviders() []provider.UpstreamLDAPIdentityProviderI +} + +type UpstreamIdentityProvidersLister interface { + UpstreamOIDCIdentityProvidersLister + UpstreamLDAPIdentityProvidersLister } func GrantScopeIfRequested(authorizeRequester fosite.AuthorizeRequester, scopeName string) { diff --git a/internal/oidc/oidctestutil/oidc.go b/internal/oidc/oidctestutil/oidc.go index 07a1c890..34938139 100644 --- a/internal/oidc/oidctestutil/oidc.go +++ b/internal/oidc/oidctestutil/oidc.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidctestutil @@ -112,16 +112,29 @@ func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(_ context.Context, _ *o panic("implement me") } -func NewIDPListGetter(upstreamOIDCIdentityProviders ...*TestUpstreamOIDCIdentityProvider) provider.DynamicUpstreamIDPProvider { +type UpstreamIDPListerBuilder struct { + upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider +} + +func (b *UpstreamIDPListerBuilder) WithOIDC(upstreamOIDCIdentityProviders ...*TestUpstreamOIDCIdentityProvider) *UpstreamIDPListerBuilder { + b.upstreamOIDCIdentityProviders = append(b.upstreamOIDCIdentityProviders, upstreamOIDCIdentityProviders...) + return b +} + +func (b *UpstreamIDPListerBuilder) Build() provider.DynamicUpstreamIDPProvider { idpProvider := provider.NewDynamicUpstreamIDPProvider() - upstreams := make([]provider.UpstreamOIDCIdentityProviderI, len(upstreamOIDCIdentityProviders)) - for i := range upstreamOIDCIdentityProviders { - upstreams[i] = provider.UpstreamOIDCIdentityProviderI(upstreamOIDCIdentityProviders[i]) + upstreams := make([]provider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) + for i := range b.upstreamOIDCIdentityProviders { + upstreams[i] = provider.UpstreamOIDCIdentityProviderI(b.upstreamOIDCIdentityProviders[i]) } - idpProvider.SetIDPList(upstreams) + idpProvider.SetOIDCIdentityProviders(upstreams) return idpProvider } +func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder { + return &UpstreamIDPListerBuilder{} +} + // Declare a separate type from the production code to ensure that the state param's contents was serialized // in the format that we expect, with the json keys that we expect, etc. This also ensure that the order of // the serialized fields is the same, which doesn't really matter expect that we can make simpler equality diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index fd367d88..1893352b 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package provider @@ -10,6 +10,7 @@ import ( "golang.org/x/oauth2" + "go.pinniped.dev/internal/ldap" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/pkce" @@ -48,30 +49,54 @@ type UpstreamOIDCIdentityProviderI interface { ValidateToken(ctx context.Context, tok *oauth2.Token, expectedIDTokenNonce nonce.Nonce) (*oidctypes.Token, error) } +type UpstreamLDAPIdentityProviderI interface { + // A name for this upstream provider. + GetName() string + + // A method for performing user authentication against the upstream LDAP provider. + ldap.UserAuthenticator +} + type DynamicUpstreamIDPProvider interface { - SetIDPList(oidcIDPs []UpstreamOIDCIdentityProviderI) - GetIDPList() []UpstreamOIDCIdentityProviderI + SetOIDCIdentityProviders(oidcIDPs []UpstreamOIDCIdentityProviderI) + GetOIDCIdentityProviders() []UpstreamOIDCIdentityProviderI + SetLDAPIdentityProviders(ldapIDPs []UpstreamLDAPIdentityProviderI) + GetLDAPIdentityProviders() []UpstreamLDAPIdentityProviderI } type dynamicUpstreamIDPProvider struct { - federationDomains []UpstreamOIDCIdentityProviderI - mutex sync.RWMutex + oidcUpstreams []UpstreamOIDCIdentityProviderI + ldapUpstreams []UpstreamLDAPIdentityProviderI + mutex sync.RWMutex } func NewDynamicUpstreamIDPProvider() DynamicUpstreamIDPProvider { return &dynamicUpstreamIDPProvider{ - federationDomains: []UpstreamOIDCIdentityProviderI{}, + oidcUpstreams: []UpstreamOIDCIdentityProviderI{}, + ldapUpstreams: []UpstreamLDAPIdentityProviderI{}, } } -func (p *dynamicUpstreamIDPProvider) SetIDPList(oidcIDPs []UpstreamOIDCIdentityProviderI) { +func (p *dynamicUpstreamIDPProvider) SetOIDCIdentityProviders(oidcIDPs []UpstreamOIDCIdentityProviderI) { p.mutex.Lock() // acquire a write lock defer p.mutex.Unlock() - p.federationDomains = oidcIDPs + p.oidcUpstreams = oidcIDPs } -func (p *dynamicUpstreamIDPProvider) GetIDPList() []UpstreamOIDCIdentityProviderI { +func (p *dynamicUpstreamIDPProvider) GetOIDCIdentityProviders() []UpstreamOIDCIdentityProviderI { p.mutex.RLock() // acquire a read lock defer p.mutex.RUnlock() - return p.federationDomains + return p.oidcUpstreams +} + +func (p *dynamicUpstreamIDPProvider) SetLDAPIdentityProviders(ldapIDPs []UpstreamLDAPIdentityProviderI) { + p.mutex.Lock() // acquire a write lock + defer p.mutex.Unlock() + p.ldapUpstreams = ldapIDPs +} + +func (p *dynamicUpstreamIDPProvider) GetLDAPIdentityProviders() []UpstreamLDAPIdentityProviderI { + p.mutex.RLock() // acquire a read lock + defer p.mutex.RUnlock() + return p.ldapUpstreams } diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 52227ce5..669e5152 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package manager @@ -8,10 +8,6 @@ import ( "strings" "sync" - "go.pinniped.dev/internal/secret" - - "go.pinniped.dev/internal/oidc/dynamiccodec" - corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "go.pinniped.dev/internal/oidc" @@ -19,10 +15,12 @@ import ( "go.pinniped.dev/internal/oidc/callback" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/discovery" + "go.pinniped.dev/internal/oidc/dynamiccodec" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/token" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/secret" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/pkce" ) @@ -33,22 +31,22 @@ import ( type Manager struct { mu sync.RWMutex providers []*provider.FederationDomainIssuer - providerHandlers map[string]http.Handler // map of all routes for all providers - nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request - dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data - idpListGetter oidc.IDPListGetter // in-memory cache of upstream IDPs - secretCache *secret.Cache // in-memory cache of cryptographic material + providerHandlers map[string]http.Handler // map of all routes for all providers + nextHandler http.Handler // the next handler in a chain, called when this manager didn't know how to handle a request + dynamicJWKSProvider jwks.DynamicJWKSProvider // in-memory cache of per-issuer JWKS data + upstreamIDPs oidc.UpstreamIdentityProvidersLister // in-memory cache of upstream IDPs + secretCache *secret.Cache // in-memory cache of cryptographic material secretsClient corev1client.SecretInterface } // NewManager returns an empty Manager. // nextHandler will be invoked for any requests that could not be handled by this manager's providers. // dynamicJWKSProvider will be used as an in-memory cache for per-issuer JWKS data. -// idpListGetter will be used as an in-memory cache of currently configured upstream IDPs. +// upstreamIDPs will be used as an in-memory cache of currently configured upstream IDPs. func NewManager( nextHandler http.Handler, dynamicJWKSProvider jwks.DynamicJWKSProvider, - idpListGetter oidc.IDPListGetter, + upstreamIDPs oidc.UpstreamIdentityProvidersLister, secretCache *secret.Cache, secretsClient corev1client.SecretInterface, ) *Manager { @@ -56,7 +54,7 @@ func NewManager( providerHandlers: make(map[string]http.Handler), nextHandler: nextHandler, dynamicJWKSProvider: dynamicJWKSProvider, - idpListGetter: idpListGetter, + upstreamIDPs: upstreamIDPs, secretCache: secretCache, secretsClient: secretsClient, } @@ -110,7 +108,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs m.providerHandlers[(issuerHostWithPath + oidc.AuthorizationEndpointPath)] = auth.NewHandler( issuer, - m.idpListGetter, + m.upstreamIDPs, oauthHelperWithNullStorage, csrftoken.Generate, pkce.Generate, @@ -120,7 +118,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs ) m.providerHandlers[(issuerHostWithPath + oidc.CallbackEndpointPath)] = callback.NewHandler( - m.idpListGetter, + m.upstreamIDPs, oauthHelperWithKubeStorage, upstreamStateEncoder, csrfCookieEncoder, diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 64b7ab4a..a5d79df9 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package manager @@ -221,7 +221,7 @@ func TestManager(t *testing.T) { parsedUpstreamIDPAuthorizationURL, err := url.Parse(upstreamIDPAuthorizationURL) r.NoError(err) - idpListGetter := oidctestutil.NewIDPListGetter(&oidctestutil.TestUpstreamOIDCIdentityProvider{ + idpLister := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{ Name: "test-idp", ClientID: "test-client-id", AuthorizationURL: *parsedUpstreamIDPAuthorizationURL, @@ -238,7 +238,7 @@ func TestManager(t *testing.T) { }, }, nil }, - }) + }).Build() kubeClient = fake.NewSimpleClientset() secretsClient := kubeClient.CoreV1().Secrets("some-namespace") @@ -254,7 +254,7 @@ func TestManager(t *testing.T) { cache.SetStateEncoderHashKey(issuer2, []byte("some-state-encoder-hash-key-2")) cache.SetStateEncoderBlockKey(issuer2, []byte("16-bytes-STATE02")) - subject = NewManager(nextHandler, dynamicJWKSProvider, idpListGetter, &cache, secretsClient) + subject = NewManager(nextHandler, dynamicJWKSProvider, idpLister, &cache, secretsClient) }) when("given no providers via SetProviders()", func() { diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 909d6b1b..93441949 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -388,7 +388,9 @@ func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAutho defer authResponse.Body.Close() require.NoError(t, err) - t.Skip("The rest of this test will not work until we implement the corresponding production code.") // TODO remove this skip + // TODO remove this skip + _ = responseBody // suppress linter until we remove the below skip + t.Skip("The rest of this test will not work until we implement the corresponding production code.") require.Equalf(t, http.StatusOK, authResponse.StatusCode, "response body was: %s", string(responseBody)) } From 064e3144a252c814e396e02063c8c6403ce56715 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 7 Apr 2021 17:05:25 -0700 Subject: [PATCH 05/59] auth_handler.go: pre-factor to make room for upstream LDAP IDPs --- internal/oidc/auth/auth_handler.go | 11 +- internal/oidc/auth/auth_handler_test.go | 117 +++++++++++----------- internal/oidc/provider/manager/manager.go | 1 + 3 files changed, 65 insertions(+), 64 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 394d8432..7b007863 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -28,7 +28,8 @@ import ( func NewHandler( downstreamIssuer string, idpLister oidc.UpstreamIdentityProvidersLister, - oauthHelper fosite.OAuth2Provider, + oauthHelperWithNullStorage fosite.OAuth2Provider, + oauthHelperWithRealStorage fosite.OAuth2Provider, generateCSRF func() (csrftoken.CSRFToken, error), generatePKCE func() (pkce.Code, error), generateNonce func() (nonce.Nonce, error), @@ -45,10 +46,10 @@ func NewHandler( csrfFromCookie := readCSRFCookie(r, cookieCodec) - authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r) + authorizeRequester, err := oauthHelperWithNullStorage.NewAuthorizeRequest(r.Context(), r) if err != nil { plog.Info("authorize request error", oidc.FositeErrorForLog(err)...) - oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + oauthHelperWithNullStorage.WriteAuthorizeError(w, authorizeRequester, err) return nil } @@ -68,7 +69,7 @@ func NewHandler( oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience") now := time.Now() - _, err = oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{ + _, err = oauthHelperWithNullStorage.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{ Claims: &jwt.IDTokenClaims{ // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. Subject: "none", @@ -78,7 +79,7 @@ func NewHandler( }) if err != nil { plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) - oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + oauthHelperWithNullStorage.WriteAuthorizeError(w, authorizeRequester, err) return nil } diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index a69fa3c4..0be99023 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -13,6 +13,8 @@ import ( "strings" "testing" + "k8s.io/client-go/kubernetes/fake" + "github.com/gorilla/securecookie" "github.com/stretchr/testify/require" @@ -101,6 +103,23 @@ func TestAuthorizationEndpoint(t *testing.T) { } ) + kubeClient := fake.NewSimpleClientset() + secretsClient := kubeClient.CoreV1().Secrets("some-namespace") + + hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } + require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes") + jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() + + // Configure fosite the same way that the production code would when using Kube storage. + // Inject this into our test subject at the last second so we get a fresh storage for every test. + timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration() + kubeOauthStore := oidc.NewKubeStorage(secretsClient, timeoutsConfiguration) + oauthHelperWithRealStorage := oidc.FositeOauth2Helper(kubeOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration) + + // Configure fosite the same way that the production code would, using NullStorage to turn off storage. + nullOauthStore := oidc.NullStorage{} + oauthHelperWithNullStorage := oidc.FositeOauth2Helper(nullOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration) + upstreamAuthURL, err := url.Parse("https://some-upstream-idp:8443/auth") require.NoError(t, err) @@ -111,19 +130,15 @@ func TestAuthorizationEndpoint(t *testing.T) { Scopes: []string{"scope1", "scope2"}, // the scopes to request when starting the upstream authorization flow } - // Configure fosite the same way that the production code would, using NullStorage to turn off storage. - oauthStore := oidc.NullStorage{} - hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } - require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes") - jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() - oauthHelper := oidc.FositeOauth2Helper(oauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, oidc.DefaultOIDCTimeoutsConfiguration()) - happyCSRF := "test-csrf" happyPKCE := "test-pkce" happyNonce := "test-nonce" happyCSRFGenerator := func() (csrftoken.CSRFToken, error) { return csrftoken.CSRFToken(happyCSRF), nil } happyPKCEGenerator := func() (pkce.Code, error) { return pkce.Code(happyPKCE), nil } happyNonceGenerator := func() (nonce.Nonce, error) { return nonce.Nonce(happyNonce), nil } + sadCSRFGenerator := func() (csrftoken.CSRFToken, error) { return "", fmt.Errorf("some csrf generator error") } + sadPKCEGenerator := func() (pkce.Code, error) { return "", fmt.Errorf("some PKCE generator error") } + sadNonceGenerator := func() (nonce.Nonce, error) { return "", fmt.Errorf("some nonce generator error") } // This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example: // $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1 @@ -218,7 +233,7 @@ func TestAuthorizationEndpoint(t *testing.T) { return encoded } - expectedRedirectLocation := func(expectedUpstreamState string, expectedPrompt string) string { + expectedRedirectLocationForUpstreamOIDC := func(expectedUpstreamState string, expectedPrompt string) string { query := map[string]string{ "response_type": "code", "access_type": "offline", @@ -243,7 +258,6 @@ func TestAuthorizationEndpoint(t *testing.T) { type testCase struct { name string - issuer string idpLister provider.DynamicUpstreamIDPProvider generateCSRF func() (csrftoken.CSRFToken, error) generatePKCE func() (pkce.Code, error) @@ -268,8 +282,7 @@ func TestAuthorizationEndpoint(t *testing.T) { } tests := []testCase{ { - name: "happy path using GET without a CSRF cookie", - issuer: downstreamIssuer, + name: "OIDC upstream happy path using GET without a CSRF cookie", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -281,13 +294,12 @@ func TestAuthorizationEndpoint(t *testing.T) { wantStatus: http.StatusFound, wantContentType: "text/html; charset=utf-8", wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam(nil, "", ""), ""), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, { - name: "happy path using GET with a CSRF cookie", - issuer: downstreamIssuer, + name: "OIDC upstream happy path using GET with a CSRF cookie", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -299,13 +311,12 @@ func TestAuthorizationEndpoint(t *testing.T) { csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", wantStatus: http.StatusFound, wantContentType: "text/html; charset=utf-8", - wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), ""), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), ""), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, { - name: "happy path using POST", - issuer: downstreamIssuer, + name: "OIDC upstream happy path using POST", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -320,12 +331,11 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "", wantBodyString: "", wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam(nil, "", ""), ""), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, }, { - name: "happy path with prompt param login passed through to redirect uri", - issuer: downstreamIssuer, + name: "OIDC upstream happy path with prompt param login passed through to redirect uri", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -340,12 +350,11 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "text/html; charset=utf-8", wantBodyStringWithLocationInHref: true, wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), "login"), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), "login"), wantUpstreamStateParamInLocationHeader: true, }, { name: "error while decoding CSRF cookie just generates a new cookie and succeeds as usual", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -359,13 +368,12 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "text/html; charset=utf-8", // Generated a new CSRF cookie and set it in the response. wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam(nil, "", ""), ""), + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, { - name: "happy path when downstream redirect uri matches what is configured for client except for the port number", - issuer: downstreamIssuer, + name: "OIDC upstream happy path when downstream redirect uri matches what is configured for client except for the port number", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -379,15 +387,14 @@ func TestAuthorizationEndpoint(t *testing.T) { wantStatus: http.StatusFound, wantContentType: "text/html; charset=utf-8", wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam(map[string]string{ + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client }, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, { - name: "happy path when downstream requested scopes include offline_access", - issuer: downstreamIssuer, + name: "OIDC upstream happy path when downstream requested scopes include offline_access", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -399,7 +406,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantStatus: http.StatusFound, wantContentType: "text/html; charset=utf-8", wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam(map[string]string{ + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ "scope": "openid offline_access", }, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, @@ -407,7 +414,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream redirect uri does not match what is configured for client", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -424,7 +430,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream client does not exist", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -439,7 +444,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "response type is unsupported", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -455,7 +459,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "downstream scopes do not match what is configured for client", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -471,7 +474,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing response type in request", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -487,7 +489,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing client id in request", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -502,7 +503,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing PKCE code_challenge in request", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -518,7 +518,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "invalid value for PKCE code_challenge_method in request", // https://tools.ietf.org/html/rfc7636#section-4.3 - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -534,7 +533,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "when PKCE code_challenge_method in request is `plain`", // https://tools.ietf.org/html/rfc7636#section-4.3 - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -550,7 +548,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "missing PKCE code_challenge_method in request", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -568,7 +565,6 @@ func TestAuthorizationEndpoint(t *testing.T) { // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running // through that part of the fosite library. name: "prompt param is not allowed to have none and another legal value at the same time", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -584,7 +580,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "OIDC validations are skipped when the openid scope was not requested", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -597,7 +592,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantStatus: http.StatusFound, wantContentType: "text/html; charset=utf-8", wantCSRFValueInCookieHeader: happyCSRF, - wantLocationHeader: expectedRedirectLocation(expectedUpstreamStateParam( + wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam( map[string]string{"prompt": "none login", "scope": "email"}, "", "", ), ""), wantUpstreamStateParamInLocationHeader: true, @@ -605,7 +600,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "state does not have enough entropy", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -621,7 +615,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error while encoding upstream state param", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -636,7 +629,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error while encoding CSRF cookie value for new cookie", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -651,9 +643,8 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error while generating CSRF token", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), - generateCSRF: func() (csrftoken.CSRFToken, error) { return "", fmt.Errorf("some csrf generator error") }, + generateCSRF: sadCSRFGenerator, generatePKCE: happyPKCEGenerator, generateNonce: happyNonceGenerator, stateEncoder: happyStateEncoder, @@ -666,11 +657,10 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error while generating nonce", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, - generateNonce: func() (nonce.Nonce, error) { return "", fmt.Errorf("some nonce generator error") }, + generateNonce: sadNonceGenerator, stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, method: http.MethodGet, @@ -681,10 +671,9 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "error while generating PKCE", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, - generatePKCE: func() (pkce.Code, error) { return "", fmt.Errorf("some PKCE generator error") }, + generatePKCE: sadPKCEGenerator, generateNonce: happyNonceGenerator, stateEncoder: happyStateEncoder, cookieEncoder: happyCookieEncoder, @@ -696,7 +685,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "no upstream providers are configured", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC().Build(), // empty method: http.MethodGet, path: happyGetRequestPath, @@ -706,7 +694,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "too many upstream providers are configured", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider, &upstreamOIDCIdentityProvider).Build(), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, @@ -716,7 +703,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "PUT is a bad method", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), method: http.MethodPut, path: "/some/path", @@ -726,7 +712,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "PATCH is a bad method", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), method: http.MethodPatch, path: "/some/path", @@ -736,7 +721,6 @@ func TestAuthorizationEndpoint(t *testing.T) { }, { name: "DELETE is a bad method", - issuer: downstreamIssuer, idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), method: http.MethodDelete, path: "/some/path", @@ -798,21 +782,36 @@ func TestAuthorizationEndpoint(t *testing.T) { } else { require.Empty(t, rsp.Header().Values("Set-Cookie")) } + + // Authorization requests for an OIDC upstream should never use Kube storage. + require.Len(t, kubeClient.Actions(), 0) } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - subject := NewHandler(test.issuer, test.idpLister, oauthHelper, test.generateCSRF, test.generatePKCE, test.generateNonce, test.stateEncoder, test.cookieEncoder) + subject := NewHandler( + downstreamIssuer, + test.idpLister, + oauthHelperWithNullStorage, oauthHelperWithRealStorage, + test.generateCSRF, test.generatePKCE, test.generateNonce, + test.stateEncoder, test.cookieEncoder, + ) runOneTestCase(t, test, subject) }) } t.Run("allows upstream provider configuration to change between requests", func(t *testing.T) { test := tests[0] - require.Equal(t, "happy path using GET without a CSRF cookie", test.name) // re-use the happy path test case + require.Equal(t, "OIDC upstream happy path using GET without a CSRF cookie", test.name) // re-use the happy path test case - subject := NewHandler(test.issuer, test.idpLister, oauthHelper, test.generateCSRF, test.generatePKCE, test.generateNonce, test.stateEncoder, test.cookieEncoder) + subject := NewHandler( + downstreamIssuer, + test.idpLister, + oauthHelperWithNullStorage, oauthHelperWithRealStorage, + test.generateCSRF, test.generatePKCE, test.generateNonce, + test.stateEncoder, test.cookieEncoder, + ) runOneTestCase(t, test, subject) diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 669e5152..f1192edb 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -110,6 +110,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs issuer, m.upstreamIDPs, oauthHelperWithNullStorage, + oauthHelperWithKubeStorage, csrftoken.Generate, pkce.Generate, nonce.Generate, From f6ded84f07d467ab0b0dabe20dc32fb9ae5caa96 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 8 Apr 2021 17:28:01 -0700 Subject: [PATCH 06/59] Implement upstream LDAP support in auth_handler.go - When the upstream IDP is an LDAP IDP and the user's LDAP username and password are received as new custom headers, then authenticate the user and, if authentication was successful, return a redirect with an authcode. Handle errors according to the OAuth/OIDC specs. - Still does not support having multiple upstream IDPs defined at the same time, which was an existing limitation of this endpoint. - Does not yet include the actual LDAP authentication, which is hidden behind an interface from the point of view of auth_handler.go - Move the oidctestutil package to the testutil directory. - Add an interface for Fosite storage to avoid a cyclical test dependency. - Add GetURL() to the UpstreamLDAPIdentityProviderI interface. - Extract test helpers to be shared between callback_handler_test.go and auth_handler_test.go because the authcode and fosite storage assertions should be identical. - Backfill Content-Type assertions in callback_handler_test.go. Signed-off-by: Andrew Keesler --- .../upstreamwatcher/upstreamwatcher_test.go | 2 +- .../fosite_sotrage_interface.go | 21 + internal/ldap/ldap.go | 15 + internal/oidc/auth/auth_handler.go | 317 ++++++---- internal/oidc/auth/auth_handler_test.go | 573 +++++++++++++++--- .../oidc/callback/callback_handler_test.go | 405 ++++--------- .../dynamic_open_id_connect_ecdsa_strategy.go | 7 +- ...mic_open_id_connect_ecdsa_strategy_test.go | 4 +- internal/oidc/kube_storage.go | 5 +- internal/oidc/nullstorage.go | 5 +- internal/oidc/oidctestutil/oidc.go | 187 ------ .../provider/dynamic_upstream_idp_provider.go | 5 + .../oidc/provider/manager/manager_test.go | 2 +- internal/oidc/token/token_handler_test.go | 45 +- .../testutil/oidctestutil/oidctestutil.go | 469 ++++++++++++++ 15 files changed, 1360 insertions(+), 702 deletions(-) create mode 100644 internal/fositestoragei/fosite_sotrage_interface.go delete mode 100644 internal/oidc/oidctestutil/oidc.go create mode 100644 internal/testutil/oidctestutil/oidctestutil.go diff --git a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go index 0b199a11..bb0cf454 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go @@ -24,9 +24,9 @@ import ( pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/controllerlib" - "go.pinniped.dev/internal/oidc/oidctestutil" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/internal/testutil/testlogger" "go.pinniped.dev/internal/upstreamoidc" ) diff --git a/internal/fositestoragei/fosite_sotrage_interface.go b/internal/fositestoragei/fosite_sotrage_interface.go new file mode 100644 index 00000000..408dfb28 --- /dev/null +++ b/internal/fositestoragei/fosite_sotrage_interface.go @@ -0,0 +1,21 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package fositestoragei + +import ( + "github.com/ory/fosite" + "github.com/ory/fosite/handler/oauth2" + "github.com/ory/fosite/handler/openid" + "github.com/ory/fosite/handler/pkce" +) + +// This interface seems to be missing from Fosite. +// Not having this interface makes it a pain to avoid cyclical test dependencies, so we'll define it. +type AllFositeStorage interface { + fosite.ClientManager + oauth2.CoreStorage + oauth2.TokenRevocationStorage + openid.OpenIDConnectRequestStorage + pkce.PKCERequestStorage +} diff --git a/internal/ldap/ldap.go b/internal/ldap/ldap.go index 182971d4..a5899be2 100644 --- a/internal/ldap/ldap.go +++ b/internal/ldap/ldap.go @@ -13,6 +13,21 @@ import ( // This interface is similar to the k8s token authenticator, but works with username/passwords instead // of a single token string. // +// The return values should be as follows. +// 1. For a successful authentication: +// - A response which includes the username, uid, and groups in the userInfo. The username and uid must not be blank. +// - true +// - nil error +// 2. For an unsuccessful authentication, e.g. bad username or password: +// - nil response +// - false +// - nil error +// 3. For an unexpected error, e.g. a network problem: +// - nil response +// - false +// - an error +// Other combinations of return values must be avoided. +// // See k8s.io/apiserver/pkg/authentication/authenticator/interfaces.go for the token authenticator // interface, as well as the Response type. type UserAuthenticator interface { diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 7b007863..701d64da 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -13,6 +13,7 @@ import ( "github.com/ory/fosite" "github.com/ory/fosite/handler/openid" "github.com/ory/fosite/token/jwt" + "github.com/pkg/errors" "golang.org/x/oauth2" "go.pinniped.dev/internal/httputil/httperr" @@ -25,11 +26,16 @@ import ( "go.pinniped.dev/pkg/oidcclient/pkce" ) +const ( + CustomUsernameHeaderName = "X-Pinniped-Upstream-Username" + CustomPasswordHeaderName = "X-Pinniped-Upstream-Password" //nolint:gosec // this is not a credential +) + func NewHandler( downstreamIssuer string, idpLister oidc.UpstreamIdentityProvidersLister, - oauthHelperWithNullStorage fosite.OAuth2Provider, - oauthHelperWithRealStorage fosite.OAuth2Provider, + oauthHelperWithoutStorage fosite.OAuth2Provider, + oauthHelperWithStorage fosite.OAuth2Provider, generateCSRF func() (csrftoken.CSRFToken, error), generatePKCE func() (pkce.Code, error), generateNonce func() (nonce.Nonce, error), @@ -44,109 +50,203 @@ func NewHandler( return httperr.Newf(http.StatusMethodNotAllowed, "%s (try GET or POST)", r.Method) } - csrfFromCookie := readCSRFCookie(r, cookieCodec) - - authorizeRequester, err := oauthHelperWithNullStorage.NewAuthorizeRequest(r.Context(), r) - if err != nil { - plog.Info("authorize request error", oidc.FositeErrorForLog(err)...) - oauthHelperWithNullStorage.WriteAuthorizeError(w, authorizeRequester, err) - return nil - } - - upstreamIDP, err := chooseUpstreamIDP(idpLister) + oidcUpstream, ldapUpstream, err := chooseUpstreamIDP(idpLister) if err != nil { plog.WarningErr("authorize upstream config", err) return err } - // Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations. - oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID) - // There don't seem to be any validations inside `NewAuthorizeResponse` related to the offline_access scope - // at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite. - oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess) - - // Grant the pinniped:request-audience scope if requested. - oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience") - - now := time.Now() - _, err = oauthHelperWithNullStorage.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{ - Claims: &jwt.IDTokenClaims{ - // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. - Subject: "none", - AuthTime: now, - RequestedAt: now, - }, - }) - if err != nil { - plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) - oauthHelperWithNullStorage.WriteAuthorizeError(w, authorizeRequester, err) - return nil + if oidcUpstream != nil { + return handleAuthRequestForOIDCUpstream(r, w, + oauthHelperWithoutStorage, + generateCSRF, generateNonce, generatePKCE, + oidcUpstream, + downstreamIssuer, + upstreamStateEncoder, + cookieCodec, + ) } - - csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) - if err != nil { - plog.Error("authorize generate error", err) - return err - } - if csrfFromCookie != "" { - csrfValue = csrfFromCookie - } - - upstreamOAuthConfig := oauth2.Config{ - ClientID: upstreamIDP.GetClientID(), - Endpoint: oauth2.Endpoint{ - AuthURL: upstreamIDP.GetAuthorizationURL().String(), - }, - RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuer), - Scopes: upstreamIDP.GetScopes(), - } - - encodedStateParamValue, err := upstreamStateParam( - authorizeRequester, - upstreamIDP.GetName(), - nonceValue, - csrfValue, - pkceValue, - upstreamStateEncoder, + return handleAuthRequestForLDAPUpstream(r, w, + oauthHelperWithStorage, + ldapUpstream, ) - if err != nil { - plog.Error("authorize upstream state param error", err) - return err - } - - if csrfFromCookie == "" { - // We did not receive an incoming CSRF cookie, so write a new one. - err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) - if err != nil { - plog.Error("error setting CSRF cookie", err) - return err - } - } - - authCodeOptions := []oauth2.AuthCodeOption{ - oauth2.AccessTypeOffline, - nonceValue.Param(), - pkceValue.Challenge(), - pkceValue.Method(), - } - - promptParam := r.Form.Get("prompt") - if promptParam != "" && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { - authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("prompt", promptParam)) - } - - http.Redirect(w, r, - upstreamOAuthConfig.AuthCodeURL( - encodedStateParamValue, - authCodeOptions..., - ), - 302, - ) - - return nil })) } +func handleAuthRequestForLDAPUpstream( + r *http.Request, + w http.ResponseWriter, + oauthHelper fosite.OAuth2Provider, + ldapUpstream provider.UpstreamLDAPIdentityProviderI, +) error { + authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper) + if !created { + return nil + } + + username := r.Header.Get(CustomUsernameHeaderName) + password := r.Header.Get(CustomPasswordHeaderName) + if username == "" || password == "" { + // Return an error according to OIDC spec 3.1.2.6 (second paragraph). + err := errors.WithStack(fosite.ErrAccessDenied.WithHintf("Missing or blank username or password.")) + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + return nil + } + + authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password) + if err != nil { + plog.WarningErr("unexpected error during upstream authentication", err, "upstreamName", ldapUpstream.GetName()) + return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication") + } + if !authenticated { + // Return an error according to OIDC spec 3.1.2.6 (second paragraph). + err = errors.WithStack(fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider.")) + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + return nil + } + + subject := fmt.Sprintf("%s?%s=%s", ldapUpstream.GetURL(), oidc.IDTokenSubjectClaim, authenticateResponse.User.GetUID()) + now := time.Now().UTC() + openIDSession := &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + Subject: subject, + RequestedAt: now, + AuthTime: now, + }, + } + openIDSession.Claims.Extra = map[string]interface{}{ + oidc.DownstreamUsernameClaim: authenticateResponse.User.GetName(), + oidc.DownstreamGroupsClaim: authenticateResponse.User.GetGroups(), + } + + authorizeResponder, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, openIDSession) + if err != nil { + plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + return nil + } + + oauthHelper.WriteAuthorizeResponse(w, authorizeRequester, authorizeResponder) + return nil +} + +func handleAuthRequestForOIDCUpstream( + r *http.Request, + w http.ResponseWriter, + oauthHelper fosite.OAuth2Provider, + generateCSRF func() (csrftoken.CSRFToken, error), + generateNonce func() (nonce.Nonce, error), + generatePKCE func() (pkce.Code, error), + oidcUpstream provider.UpstreamOIDCIdentityProviderI, + downstreamIssuer string, + upstreamStateEncoder oidc.Encoder, + cookieCodec oidc.Codec, +) error { + authorizeRequester, created := newAuthorizeRequest(r, w, oauthHelper) + if !created { + return nil + } + + now := time.Now() + _, err := oauthHelper.NewAuthorizeResponse(r.Context(), authorizeRequester, &openid.DefaultSession{ + Claims: &jwt.IDTokenClaims{ + // Temporary claim values to allow `NewAuthorizeResponse` to perform other OIDC validations. + Subject: "none", + AuthTime: now, + RequestedAt: now, + }, + }) + if err != nil { + plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + return nil + } + + csrfValue, nonceValue, pkceValue, err := generateValues(generateCSRF, generateNonce, generatePKCE) + if err != nil { + plog.Error("authorize generate error", err) + return err + } + csrfFromCookie := readCSRFCookie(r, cookieCodec) + if csrfFromCookie != "" { + csrfValue = csrfFromCookie + } + + upstreamOAuthConfig := oauth2.Config{ + ClientID: oidcUpstream.GetClientID(), + Endpoint: oauth2.Endpoint{ + AuthURL: oidcUpstream.GetAuthorizationURL().String(), + }, + RedirectURL: fmt.Sprintf("%s/callback", downstreamIssuer), + Scopes: oidcUpstream.GetScopes(), + } + + encodedStateParamValue, err := upstreamStateParam( + authorizeRequester, + oidcUpstream.GetName(), + nonceValue, + csrfValue, + pkceValue, + upstreamStateEncoder, + ) + if err != nil { + plog.Error("authorize upstream state param error", err) + return err + } + + if csrfFromCookie == "" { + // We did not receive an incoming CSRF cookie, so write a new one. + err := addCSRFSetCookieHeader(w, csrfValue, cookieCodec) + if err != nil { + plog.Error("error setting CSRF cookie", err) + return err + } + } + + authCodeOptions := []oauth2.AuthCodeOption{ + oauth2.AccessTypeOffline, + nonceValue.Param(), + pkceValue.Challenge(), + pkceValue.Method(), + } + + promptParam := r.Form.Get("prompt") + if promptParam != "" && oidc.ScopeWasRequested(authorizeRequester, coreosoidc.ScopeOpenID) { + authCodeOptions = append(authCodeOptions, oauth2.SetAuthURLParam("prompt", promptParam)) + } + + http.Redirect(w, r, + upstreamOAuthConfig.AuthCodeURL( + encodedStateParamValue, + authCodeOptions..., + ), + 302, + ) + + return nil +} + +func newAuthorizeRequest(r *http.Request, w http.ResponseWriter, oauthHelper fosite.OAuth2Provider) (fosite.AuthorizeRequester, bool) { + authorizeRequester, err := oauthHelper.NewAuthorizeRequest(r.Context(), r) + if err != nil { + plog.Info("authorize request error", oidc.FositeErrorForLog(err)...) + oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) + return nil, false + } + grantScopes(authorizeRequester) + return authorizeRequester, true +} + +func grantScopes(authorizeRequester fosite.AuthorizeRequester) { + // Grant the openid scope (for now) if they asked for it so that `NewAuthorizeResponse` will perform its OIDC validations. + oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOpenID) + // There don't seem to be any validations inside `NewAuthorizeResponse` related to the offline_access scope + // at this time, however we will temporarily grant the scope just in case that changes in a future release of fosite. + oidc.GrantScopeIfRequested(authorizeRequester, coreosoidc.ScopeOfflineAccess) + // Grant the pinniped:request-audience scope if requested. + oidc.GrantScopeIfRequested(authorizeRequester, "pinniped:request-audience") +} + func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { receivedCSRFCookie, err := r.Cookie(oidc.CSRFCookieName) if err != nil { @@ -166,27 +266,34 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { return csrfFromCookie } -func chooseUpstreamIDP(idpLister oidc.UpstreamOIDCIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, error) { - allUpstreamIDPs := idpLister.GetOIDCIdentityProviders() - if len(allUpstreamIDPs) == 0 { - return nil, httperr.New( +// Select either an OIDC or an LDAP IDP, or return an error. +func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, error) { + oidcUpstreams := idpLister.GetOIDCIdentityProviders() + ldapUpstreams := idpLister.GetLDAPIdentityProviders() + switch { + case len(oidcUpstreams)+len(ldapUpstreams) == 0: + return nil, nil, httperr.New( http.StatusUnprocessableEntity, "No upstream providers are configured", ) - } else if len(allUpstreamIDPs) > 1 { + case len(oidcUpstreams)+len(ldapUpstreams) > 1: var upstreamIDPNames []string - for _, idp := range allUpstreamIDPs { + for _, idp := range oidcUpstreams { + upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) + } + for _, idp := range ldapUpstreams { upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) } - plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames) - - return nil, httperr.New( + return nil, nil, httperr.New( http.StatusUnprocessableEntity, "Too many upstream providers are configured (support for multiple upstreams is not yet implemented)", ) + case len(oidcUpstreams) == 1: + return oidcUpstreams[0], nil, nil + default: + return nil, ldapUpstreams[0], nil } - return allUpstreamIDPs[0], nil } func generateValues( diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 0be99023..2a597881 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -4,6 +4,7 @@ package auth import ( + "context" "fmt" "html" "net/http" @@ -13,18 +14,21 @@ import ( "strings" "testing" - "k8s.io/client-go/kubernetes/fake" - "github.com/gorilla/securecookie" + "github.com/ory/fosite" "github.com/stretchr/testify/require" + "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" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/oidctestutil" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/pkce" ) @@ -34,7 +38,13 @@ func TestAuthorizationEndpoint(t *testing.T) { downstreamIssuer = "https://my-downstream-issuer.com/some-path" downstreamRedirectURI = "http://127.0.0.1/callback" downstreamRedirectURIWithDifferentPort = "http://127.0.0.1:42/callback" + downstreamNonce = "some-nonce-value" + downstreamPKCEChallenge = "some-challenge" + downstreamPKCEChallengeMethod = "S256" happyState = "8b-state" + downstreamClientID = "pinniped-cli" + upstreamLDAPURL = "ldaps://some-ldap-host:123" + htmlContentType = "text/html; charset=utf-8" ) require.Len(t, happyState, 8, "we expect fosite to allow 8 byte state params, so we want to test that boundary case") @@ -101,20 +111,31 @@ func TestAuthorizationEndpoint(t *testing.T) { "error_description": "The authorization server does not support obtaining a token using this method. `The request is missing the 'response_type' parameter.", "state": happyState, } - ) - kubeClient := fake.NewSimpleClientset() - secretsClient := kubeClient.CoreV1().Secrets("some-namespace") + fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery = map[string]string{ + "error": "access_denied", + "error_description": "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", + "state": happyState, + } + + fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery = map[string]string{ + "error": "access_denied", + "error_description": "The resource owner or authorization server denied the request. Missing or blank username or password.", + "state": happyState, + } + ) hmacSecretFunc := func() []byte { return []byte("some secret - must have at least 32 bytes") } require.GreaterOrEqual(t, len(hmacSecretFunc()), 32, "fosite requires that hmac secrets have at least 32 bytes") jwksProviderIsUnused := jwks.NewDynamicJWKSProvider() - - // Configure fosite the same way that the production code would when using Kube storage. - // Inject this into our test subject at the last second so we get a fresh storage for every test. timeoutsConfiguration := oidc.DefaultOIDCTimeoutsConfiguration() - kubeOauthStore := oidc.NewKubeStorage(secretsClient, timeoutsConfiguration) - oauthHelperWithRealStorage := oidc.FositeOauth2Helper(kubeOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration) + + createOauthHelperWithRealStorage := func(secretsClient v1.SecretInterface) (fosite.OAuth2Provider, *oidc.KubeStorage) { + // Configure fosite the same way that the production code would when using Kube storage. + // Inject this into our test subject at the last second so we get a fresh storage for every test. + kubeOauthStore := oidc.NewKubeStorage(secretsClient, timeoutsConfiguration) + return oidc.FositeOauth2Helper(kubeOauthStore, downstreamIssuer, hmacSecretFunc, jwksProviderIsUnused, timeoutsConfiguration), kubeOauthStore + } // Configure fosite the same way that the production code would, using NullStorage to turn off storage. nullOauthStore := oidc.NullStorage{} @@ -124,12 +145,45 @@ func TestAuthorizationEndpoint(t *testing.T) { require.NoError(t, err) upstreamOIDCIdentityProvider := oidctestutil.TestUpstreamOIDCIdentityProvider{ - Name: "some-idp", + Name: "some-oidc-idp", ClientID: "some-client-id", AuthorizationURL: *upstreamAuthURL, Scopes: []string{"scope1", "scope2"}, // the scopes to request when starting the upstream authorization flow } + happyLDAPUsername := "some-ldap-user" + happyLDAPUsernameFromAuthenticator := "some-mapped-ldap-username" + happyLDAPPassword := "some-ldap-password" //nolint:gosec + happyLDAPUID := "some-ldap-uid" + happyLDAPGroups := []string{"group1", "group2", "group3"} + + upstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ + Name: "some-ldap-idp", + URL: upstreamLDAPURL, + AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.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{ + User: &user.DefaultInfo{ + Name: happyLDAPUsernameFromAuthenticator, + UID: happyLDAPUID, + Groups: happyLDAPGroups, + }, + }, true, nil + } + return nil, false, nil + }, + } + + erroringUpstreamLDAPIdentityProvider := oidctestutil.TestUpstreamLDAPIdentityProvider{ + Name: "some-ldap-idp", + AuthenticateFunc: func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { + return nil, false, fmt.Errorf("some ldap upstream auth error") + }, + } + happyCSRF := "test-csrf" happyPKCE := "test-pkce" happyNonce := "test-nonce" @@ -177,14 +231,17 @@ func TestAuthorizationEndpoint(t *testing.T) { return urlToReturn } + happyDownstreamScopesRequested := []string{"openid", "profile", "email"} + happyDownstreamScopesGranted := []string{"openid"} + happyGetRequestQueryMap := map[string]string{ "response_type": "code", - "scope": "openid profile email", - "client_id": "pinniped-cli", + "scope": strings.Join(happyDownstreamScopesRequested, " "), + "client_id": downstreamClientID, "state": happyState, - "nonce": "some-nonce-value", - "code_challenge": "some-challenge", - "code_challenge_method": "S256", + "nonce": downstreamNonce, + "code_challenge": downstreamPKCEChallenge, + "code_challenge_method": downstreamPKCEChallengeMethod, "redirect_uri": downstreamRedirectURI, } @@ -242,7 +299,7 @@ func TestAuthorizationEndpoint(t *testing.T) { "state": expectedUpstreamState, "nonce": happyNonce, "code_challenge": expectedUpstreamCodeChallenge, - "code_challenge_method": "S256", + "code_challenge_method": downstreamPKCEChallengeMethod, "redirect_uri": downstreamIssuer + "/callback", } if expectedPrompt != "" { @@ -251,6 +308,9 @@ func TestAuthorizationEndpoint(t *testing.T) { return urlWithQuery(upstreamAuthURL.String(), query) } + // Note that fosite puts the granted scopes as a param in the redirect URI even though the spec doesn't seem to require it + happyAuthcodeDownstreamRedirectLocationRegexp := downstreamRedirectURI + `\?code=([^&]+)&scope=openid&state=` + happyState + incomingCookieCSRFValue := "csrf-value-from-cookie" encodedIncomingCookieCSRFValue, err := happyCookieEncoder.Encode("csrf", incomingCookieCSRFValue) require.NoError(t, err) @@ -258,27 +318,41 @@ func TestAuthorizationEndpoint(t *testing.T) { type testCase struct { name string - idpLister provider.DynamicUpstreamIDPProvider - generateCSRF func() (csrftoken.CSRFToken, error) - generatePKCE func() (pkce.Code, error) - generateNonce func() (nonce.Nonce, error) - stateEncoder oidc.Codec - cookieEncoder oidc.Codec - method string - path string - contentType string - body string - csrfCookie string + idpLister provider.DynamicUpstreamIDPProvider + generateCSRF func() (csrftoken.CSRFToken, error) + generatePKCE func() (pkce.Code, error) + generateNonce func() (nonce.Nonce, error) + stateEncoder oidc.Codec + cookieEncoder oidc.Codec + method string + path string + contentType string + body string + csrfCookie string + customUsernameHeader *string // nil means do not send header, empty means send header with empty value + customPasswordHeader *string // nil means do not send header, empty means send header with empty value - wantStatus int - wantContentType string - wantBodyString string - wantBodyJSON string - wantLocationHeader string - wantCSRFValueInCookieHeader string - - wantUpstreamStateParamInLocationHeader bool + wantStatus int + wantContentType string + wantBodyString string + wantBodyJSON string + wantCSRFValueInCookieHeader string wantBodyStringWithLocationInHref bool + wantLocationHeader string + wantUpstreamStateParamInLocationHeader bool + + // For when the request was authenticated by an upstream LDAP provider and an authcode is being returned. + wantRedirectLocationRegexp string + wantDownstreamRedirectURI string + wantDownstreamGrantedScopes []string + wantDownstreamIDTokenSubject string + wantDownstreamIDTokenUsername string + wantDownstreamIDTokenGroups []string + wantDownstreamRequestedScopes []string + wantDownstreamPKCEChallenge string + wantDownstreamPKCEChallengeMethod string + wantDownstreamNonce string + wantUnnecessaryStoredRecords int } tests := []testCase{ { @@ -292,12 +366,33 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: happyGetRequestPath, wantStatus: http.StatusFound, - wantContentType: "text/html; charset=utf-8", + wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, + { + name: "LDAP upstream happy path using GET", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantBodyStringWithLocationInHref: false, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "?sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + }, { name: "OIDC upstream happy path using GET with a CSRF cookie", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), @@ -310,7 +405,7 @@ func TestAuthorizationEndpoint(t *testing.T) { path: happyGetRequestPath, csrfCookie: "__Host-pinniped-csrf=" + encodedIncomingCookieCSRFValue + " ", wantStatus: http.StatusFound, - wantContentType: "text/html; charset=utf-8", + wantContentType: htmlContentType, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, incomingCookieCSRFValue, ""), ""), wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, @@ -334,6 +429,29 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), wantUpstreamStateParamInLocationHeader: true, }, + { + name: "LDAP upstream happy path using POST", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodPost, + path: "/some/path", + contentType: "application/x-www-form-urlencoded", + body: encodeQuery(happyGetRequestQueryMap), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, + wantBodyStringWithLocationInHref: false, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "?sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + }, { name: "OIDC upstream happy path with prompt param login passed through to redirect uri", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), @@ -347,14 +465,14 @@ func TestAuthorizationEndpoint(t *testing.T) { contentType: "application/x-www-form-urlencoded", body: encodeQuery(happyGetRequestQueryMap), wantStatus: http.StatusFound, - wantContentType: "text/html; charset=utf-8", + wantContentType: htmlContentType, wantBodyStringWithLocationInHref: true, wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{"prompt": "login"}, "", ""), "login"), wantUpstreamStateParamInLocationHeader: true, }, { - name: "error while decoding CSRF cookie just generates a new cookie and succeeds as usual", + name: "OIDC upstream with error while decoding CSRF cookie just generates a new cookie and succeeds as usual", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -365,7 +483,7 @@ func TestAuthorizationEndpoint(t *testing.T) { path: happyGetRequestPath, csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped", wantStatus: http.StatusFound, - wantContentType: "text/html; charset=utf-8", + wantContentType: htmlContentType, // Generated a new CSRF cookie and set it in the response. wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(nil, "", ""), ""), @@ -385,7 +503,7 @@ func TestAuthorizationEndpoint(t *testing.T) { "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client }), wantStatus: http.StatusFound, - wantContentType: "text/html; charset=utf-8", + wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client @@ -393,6 +511,29 @@ func TestAuthorizationEndpoint(t *testing.T) { wantUpstreamStateParamInLocationHeader: true, wantBodyStringWithLocationInHref: true, }, + { + name: "LDAP upstream happy path when downstream redirect uri matches what is configured for client except for the port number", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{ + "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client + }), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState, + wantBodyStringWithLocationInHref: false, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "?sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: happyDownstreamScopesRequested, + wantDownstreamRedirectURI: downstreamRedirectURIWithDifferentPort, + wantDownstreamGrantedScopes: happyDownstreamScopesGranted, + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + }, { name: "OIDC upstream happy path when downstream requested scopes include offline_access", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), @@ -404,7 +545,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid offline_access"}), wantStatus: http.StatusFound, - wantContentType: "text/html; charset=utf-8", + wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam(map[string]string{ "scope": "openid offline_access", @@ -413,7 +554,66 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyStringWithLocationInHref: true, }, { - name: "downstream redirect uri does not match what is configured for client", + name: "error during upstream LDAP authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusBadGateway, + wantContentType: htmlContentType, + wantBodyString: "Bad Gateway: unexpected error during upstream authentication\n", + }, + { + name: "wrong upstream password for LDAP authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr("wrong-password"), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, + { + name: "wrong upstream username for LDAP authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: stringPtr("wrong-username"), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, + { + name: "missing upstream username on request for LDAP authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: nil, // do not send header + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, + { + name: "missing upstream password on request for LDAP authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: nil, // do not send header + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, + { + name: "downstream redirect uri does not match what is configured for client when using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -429,7 +629,20 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, { - name: "downstream client does not exist", + name: "downstream redirect uri does not match what is configured for client when using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{ + "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", + }), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusBadRequest, + wantContentType: "application/json; charset=utf-8", + wantBodyJSON: fositeInvalidRedirectURIErrorBody, + }, + { + name: "downstream client does not exist when using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -443,7 +656,16 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyJSON: fositeInvalidClientErrorBody, }, { - name: "response type is unsupported", + name: "downstream client does not exist when using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"client_id": "invalid-client"}), + wantStatus: http.StatusUnauthorized, + wantContentType: "application/json; charset=utf-8", + wantBodyJSON: fositeInvalidClientErrorBody, + }, + { + name: "response type is unsupported when using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -458,7 +680,17 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "downstream scopes do not match what is configured for client", + name: "response type is unsupported when using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": "unsupported"}), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), + wantBodyString: "", + }, + { + name: "downstream scopes do not match what is configured for client using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -473,7 +705,19 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "missing response type in request", + name: "downstream scopes do not match what is configured for client using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), + wantBodyString: "", + }, + { + name: "missing response type in request using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -488,7 +732,17 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "missing client id in request", + name: "missing response type in request using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), + wantBodyString: "", + }, + { + name: "missing client id in request using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -502,7 +756,16 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyJSON: fositeInvalidClientErrorBody, }, { - name: "missing PKCE code_challenge in request", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 + name: "missing client id in request using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"client_id": ""}), + wantStatus: http.StatusUnauthorized, + wantContentType: "application/json; charset=utf-8", + wantBodyJSON: fositeInvalidClientErrorBody, + }, + { + name: "missing PKCE code_challenge in request using OIDC upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -517,7 +780,20 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "invalid value for PKCE code_challenge_method in request", // https://tools.ietf.org/html/rfc7636#section-4.3 + name: "missing PKCE code_challenge in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, + { + name: "invalid value for PKCE code_challenge_method in request using OIDC upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -532,7 +808,20 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "when PKCE code_challenge_method in request is `plain`", // https://tools.ietf.org/html/rfc7636#section-4.3 + name: "invalid value for PKCE code_challenge_method in request using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, + { + name: "when PKCE code_challenge_method in request is `plain` using OIDC upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -547,7 +836,20 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "missing PKCE code_challenge_method in request", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 + name: "when PKCE code_challenge_method in request is `plain` using LDAP upstream", // https://tools.ietf.org/html/rfc7636#section-4.3 + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, + { + name: "missing PKCE code_challenge_method in request using OIDC upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -561,10 +863,23 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), wantBodyString: "", }, + { + name: "missing PKCE code_challenge_method in request using LDAP upstream", // See https://tools.ietf.org/html/rfc7636#section-4.4.1 + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 2, // fosite already stored the authcode and oidc session before it noticed the error + }, { // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running - // through that part of the fosite library. - name: "prompt param is not allowed to have none and another legal value at the same time", + // through that part of the fosite library when using an OIDC upstream. + name: "prompt param is not allowed to have none and another legal value at the same time using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -579,7 +894,22 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "OIDC validations are skipped when the openid scope was not requested", + // This is just one of the many OIDC validations run by fosite. This test is to ensure that we are running + // through that part of the fosite library when using an LDAP upstream. + name: "prompt param is not allowed to have none and another legal value at the same time using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), + wantBodyString: "", + wantUnnecessaryStoredRecords: 1, // fosite already stored the authcode before it noticed the error + }, + { + name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -590,7 +920,7 @@ func TestAuthorizationEndpoint(t *testing.T) { // The following prompt value is illegal when openid is requested, but note that openid is not requested. path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}), wantStatus: http.StatusFound, - wantContentType: "text/html; charset=utf-8", + wantContentType: htmlContentType, wantCSRFValueInCookieHeader: happyCSRF, wantLocationHeader: expectedRedirectLocationForUpstreamOIDC(expectedUpstreamStateParam( map[string]string{"prompt": "none login", "scope": "email"}, "", "", @@ -599,7 +929,29 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyStringWithLocationInHref: true, }, { - name: "state does not have enough entropy", + name: "happy path: downstream OIDC validations are skipped when the openid scope was not requested using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + // The following prompt value is illegal when openid is requested, but note that openid is not requested. + path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: htmlContentType, + wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyState, // no scopes granted + wantBodyStringWithLocationInHref: false, + wantDownstreamIDTokenSubject: upstreamLDAPURL + "?sub=" + happyLDAPUID, + wantDownstreamIDTokenUsername: happyLDAPUsernameFromAuthenticator, + wantDownstreamIDTokenGroups: happyLDAPGroups, + wantDownstreamRequestedScopes: []string{"email"}, // only email was requested + wantDownstreamRedirectURI: downstreamRedirectURI, + wantDownstreamGrantedScopes: []string{}, // no scopes granted + wantDownstreamNonce: downstreamNonce, + wantDownstreamPKCEChallenge: downstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, + }, + { + name: "downstream state does not have enough entropy using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -614,7 +966,19 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "", }, { - name: "error while encoding upstream state param", + name: "downstream state does not have enough entropy using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}), + customUsernameHeader: stringPtr(happyLDAPUsername), + customPasswordHeader: stringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), + wantBodyString: "", + }, + { + name: "error while encoding upstream state param using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -628,7 +992,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Internal Server Error: error encoding upstream state param\n", }, { - name: "error while encoding CSRF cookie value for new cookie", + name: "error while encoding CSRF cookie value for new cookie using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -642,7 +1006,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Internal Server Error: error encoding CSRF cookie\n", }, { - name: "error while generating CSRF token", + name: "error while generating CSRF token using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: sadCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -656,7 +1020,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Internal Server Error: error generating CSRF token\n", }, { - name: "error while generating nonce", + name: "error while generating nonce using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: happyPKCEGenerator, @@ -670,7 +1034,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Internal Server Error: error generating nonce param\n", }, { - name: "error while generating PKCE", + name: "error while generating PKCE using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), generateCSRF: happyCSRFGenerator, generatePKCE: sadPKCEGenerator, @@ -693,7 +1057,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantBodyString: "Unprocessable Entity: No upstream providers are configured\n", }, { - name: "too many upstream providers are configured", + name: "too many upstream providers are configured: multiple OIDC", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider, &upstreamOIDCIdentityProvider).Build(), // more than one not allowed method: http.MethodGet, path: happyGetRequestPath, @@ -701,6 +1065,24 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "text/plain; charset=utf-8", wantBodyString: "Unprocessable Entity: Too many upstream providers are configured (support for multiple upstreams is not yet implemented)\n", }, + { + name: "too many upstream providers are configured: multiple LDAP", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider, &upstreamLDAPIdentityProvider).Build(), // more than one not allowed + method: http.MethodGet, + path: happyGetRequestPath, + wantStatus: http.StatusUnprocessableEntity, + wantContentType: "text/plain; charset=utf-8", + wantBodyString: "Unprocessable Entity: Too many upstream providers are configured (support for multiple upstreams is not yet implemented)\n", + }, + { + name: "too many upstream providers are configured: both OIDC and LDAP", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).WithLDAP(&upstreamLDAPIdentityProvider).Build(), // more than one not allowed + method: http.MethodGet, + path: happyGetRequestPath, + wantStatus: http.StatusUnprocessableEntity, + wantContentType: "text/plain; charset=utf-8", + wantBodyString: "Unprocessable Entity: Too many upstream providers are configured (support for multiple upstreams is not yet implemented)\n", + }, { name: "PUT is a bad method", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), @@ -730,12 +1112,18 @@ func TestAuthorizationEndpoint(t *testing.T) { }, } - runOneTestCase := func(t *testing.T, test testCase, subject http.Handler) { + runOneTestCase := func(t *testing.T, test testCase, subject http.Handler, kubeOauthStore *oidc.KubeStorage, kubeClient *fake.Clientset, secretsClient v1.SecretInterface) { req := httptest.NewRequest(test.method, test.path, strings.NewReader(test.body)) req.Header.Set("Content-Type", test.contentType) if test.csrfCookie != "" { req.Header.Set("Cookie", test.csrfCookie) } + if test.customUsernameHeader != nil { + req.Header.Set("X-Pinniped-Upstream-Username", *test.customUsernameHeader) + } + if test.customPasswordHeader != nil { + req.Header.Set("X-Pinniped-Upstream-Password", *test.customPasswordHeader) + } rsp := httptest.NewRecorder() subject.ServeHTTP(rsp, req) t.Logf("response: %#v", rsp) @@ -746,7 +1134,8 @@ func TestAuthorizationEndpoint(t *testing.T) { testutil.RequireSecurityHeaders(t, rsp) actualLocation := rsp.Header().Get("Location") - if test.wantLocationHeader != "" { + switch { + case test.wantLocationHeader != "": if test.wantUpstreamStateParamInLocationHeader { requireEqualDecodedStateParams(t, actualLocation, test.wantLocationHeader, test.stateEncoder) } @@ -754,7 +1143,34 @@ func TestAuthorizationEndpoint(t *testing.T) { // compare those states since they may be different, but we do want to compare the downstream // state param that should be exactly the same. requireEqualURLs(t, actualLocation, test.wantLocationHeader, test.wantUpstreamStateParamInLocationHeader) - } else { + + // Authorization requests for either a successful OIDC upstream or for an error with any upstream + // should never use Kube storage. There is only one exception to this rule, which is that certain + // OIDC validations are checked in fosite after the OAuth authcode (and sometimes the OIDC session) + // is stored, so it is possible with an LDAP upstream to store objects and then return an error to + // the client anyway (which makes the stored objects useless, but oh well). + require.Len(t, kubeClient.Actions(), test.wantUnnecessaryStoredRecords) + case test.wantRedirectLocationRegexp != "": + require.Len(t, rsp.Header().Values("Location"), 1) + oidctestutil.RequireAuthcodeRedirectLocation( + t, + rsp.Header().Get("Location"), + test.wantRedirectLocationRegexp, + kubeClient, + secretsClient, + kubeOauthStore, + test.wantDownstreamGrantedScopes, + test.wantDownstreamIDTokenSubject, + test.wantDownstreamIDTokenUsername, + test.wantDownstreamIDTokenGroups, + test.wantDownstreamRequestedScopes, + test.wantDownstreamPKCEChallenge, + test.wantDownstreamPKCEChallengeMethod, + test.wantDownstreamNonce, + downstreamClientID, + test.wantDownstreamRedirectURI, + ) + default: require.Empty(t, rsp.Header().Values("Location")) } @@ -782,14 +1198,14 @@ func TestAuthorizationEndpoint(t *testing.T) { } else { require.Empty(t, rsp.Header().Values("Set-Cookie")) } - - // Authorization requests for an OIDC upstream should never use Kube storage. - require.Len(t, kubeClient.Actions(), 0) } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { + kubeClient := fake.NewSimpleClientset() + secretsClient := kubeClient.CoreV1().Secrets("some-namespace") + oauthHelperWithRealStorage, kubeOauthStore := createOauthHelperWithRealStorage(secretsClient) subject := NewHandler( downstreamIssuer, test.idpLister, @@ -797,7 +1213,7 @@ func TestAuthorizationEndpoint(t *testing.T) { test.generateCSRF, test.generatePKCE, test.generateNonce, test.stateEncoder, test.cookieEncoder, ) - runOneTestCase(t, test, subject) + runOneTestCase(t, test, subject, kubeOauthStore, kubeClient, secretsClient) }) } @@ -805,6 +1221,9 @@ func TestAuthorizationEndpoint(t *testing.T) { test := tests[0] require.Equal(t, "OIDC upstream happy path using GET without a CSRF cookie", test.name) // re-use the happy path test case + kubeClient := fake.NewSimpleClientset() + secretsClient := kubeClient.CoreV1().Secrets("some-namespace") + oauthHelperWithRealStorage, kubeOauthStore := createOauthHelperWithRealStorage(secretsClient) subject := NewHandler( downstreamIssuer, test.idpLister, @@ -813,7 +1232,7 @@ func TestAuthorizationEndpoint(t *testing.T) { test.stateEncoder, test.cookieEncoder, ) - runOneTestCase(t, test, subject) + runOneTestCase(t, test, subject, kubeOauthStore, kubeClient, secretsClient) // Call the setter to change the upstream IDP settings. newProviderSettings := oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -834,7 +1253,7 @@ func TestAuthorizationEndpoint(t *testing.T) { "state": expectedUpstreamStateParam(nil, "", newProviderSettings.Name), "nonce": happyNonce, "code_challenge": expectedUpstreamCodeChallenge, - "code_challenge_method": "S256", + "code_challenge_method": downstreamPKCEChallengeMethod, "redirect_uri": downstreamIssuer + "/callback", }, ) @@ -847,7 +1266,7 @@ func TestAuthorizationEndpoint(t *testing.T) { // modified expectations. This should ensure that the implementation is using the in-memory cache // of upstream IDP settings appropriately in terms of always getting the values from the cache // on every request. - runOneTestCase(t, test, subject) + runOneTestCase(t, test, subject, kubeOauthStore, kubeClient, secretsClient) }) } @@ -912,3 +1331,7 @@ func requireEqualURLs(t *testing.T, actualURL string, expectedURL string, ignore } require.Equal(t, expectedLocationQuery, actualLocationQuery) } + +func stringPtr(s string) *string { + return &s +} diff --git a/internal/oidc/callback/callback_handler_test.go b/internal/oidc/callback/callback_handler_test.go index ad82928b..c999cba4 100644 --- a/internal/oidc/callback/callback_handler_test.go +++ b/internal/oidc/callback/callback_handler_test.go @@ -9,26 +9,17 @@ import ( "net/http" "net/http/httptest" "net/url" - "regexp" "strings" "testing" - "time" "github.com/gorilla/securecookie" - "github.com/ory/fosite" - "github.com/ory/fosite/handler/openid" "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes/fake" - "go.pinniped.dev/internal/crud" - "go.pinniped.dev/internal/fositestorage/authorizationcode" - "go.pinniped.dev/internal/fositestorage/openidconnect" - "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/oidctestutil" "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" oidcpkce "go.pinniped.dev/pkg/oidcclient/pkce" @@ -44,8 +35,7 @@ const ( upstreamUsernameClaim = "the-user-claim" upstreamGroupsClaim = "the-groups-claim" - happyUpstreamAuthcode = "upstream-auth-code" - + happyUpstreamAuthcode = "upstream-auth-code" happyUpstreamRedirectURI = "https://example.com/callback" happyDownstreamState = "8b-state" @@ -61,8 +51,7 @@ const ( downstreamPKCEChallenge = "some-challenge" downstreamPKCEChallengeMethod = "S256" - authCodeExpirationSeconds = 10 * 60 // Current, we set our auth code expiration to 10 minutes - timeComparisonFudgeFactor = time.Second * 15 + htmlContentType = "text/html; charset=utf-8" ) var ( @@ -129,6 +118,7 @@ func TestCallbackEndpoint(t *testing.T) { csrfCookie string wantStatus int + wantContentType string wantBody string wantRedirectLocationRegexp string wantDownstreamGrantedScopes []string @@ -252,6 +242,7 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, wantBody: "Unprocessable Entity: email_verified claim in upstream ID token has invalid format\n", wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, @@ -264,6 +255,7 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, wantBody: "Unprocessable Entity: email_verified claim in upstream ID token has false value\n", wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, @@ -327,57 +319,64 @@ func TestCallbackEndpoint(t *testing.T) { // Pre-upstream-exchange verification { - name: "PUT method is invalid", - method: http.MethodPut, - path: newRequestPath().String(), - wantStatus: http.StatusMethodNotAllowed, - wantBody: "Method Not Allowed: PUT (try GET)\n", + name: "PUT method is invalid", + method: http.MethodPut, + path: newRequestPath().String(), + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: PUT (try GET)\n", }, { - name: "POST method is invalid", - method: http.MethodPost, - path: newRequestPath().String(), - wantStatus: http.StatusMethodNotAllowed, - wantBody: "Method Not Allowed: POST (try GET)\n", + name: "POST method is invalid", + method: http.MethodPost, + path: newRequestPath().String(), + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: POST (try GET)\n", }, { - name: "PATCH method is invalid", - method: http.MethodPatch, - path: newRequestPath().String(), - wantStatus: http.StatusMethodNotAllowed, - wantBody: "Method Not Allowed: PATCH (try GET)\n", + name: "PATCH method is invalid", + method: http.MethodPatch, + path: newRequestPath().String(), + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: PATCH (try GET)\n", }, { - name: "DELETE method is invalid", - method: http.MethodDelete, - path: newRequestPath().String(), - wantStatus: http.StatusMethodNotAllowed, - wantBody: "Method Not Allowed: DELETE (try GET)\n", + name: "DELETE method is invalid", + method: http.MethodDelete, + path: newRequestPath().String(), + wantStatus: http.StatusMethodNotAllowed, + wantContentType: htmlContentType, + wantBody: "Method Not Allowed: DELETE (try GET)\n", }, { - name: "code param was not included on request", - method: http.MethodGet, - path: newRequestPath().WithState(happyState).WithoutCode().String(), - csrfCookie: happyCSRFCookie, - wantStatus: http.StatusBadRequest, - wantBody: "Bad Request: code param not found\n", + name: "code param was not included on request", + method: http.MethodGet, + path: newRequestPath().WithState(happyState).WithoutCode().String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: code param not found\n", }, { - name: "state param was not included on request", - method: http.MethodGet, - path: newRequestPath().WithoutState().String(), - csrfCookie: happyCSRFCookie, - wantStatus: http.StatusBadRequest, - wantBody: "Bad Request: state param not found\n", + name: "state param was not included on request", + method: http.MethodGet, + path: newRequestPath().WithoutState().String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: state param not found\n", }, { - name: "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason", - idp: happyUpstream().Build(), - method: http.MethodGet, - path: newRequestPath().WithState("this-will-not-decode").String(), - csrfCookie: happyCSRFCookie, - wantStatus: http.StatusBadRequest, - wantBody: "Bad Request: error reading state\n", + name: "state param was not signed correctly, has expired, or otherwise cannot be decoded for any reason", + idp: happyUpstream().Build(), + method: http.MethodGet, + path: newRequestPath().WithState("this-will-not-decode").String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: error reading state\n", }, { // This shouldn't happen in practice because the authorize endpoint should have already run the same @@ -393,16 +392,18 @@ func TestCallbackEndpoint(t *testing.T) { csrfCookie: happyCSRFCookie, wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, wantStatus: http.StatusInternalServerError, + wantContentType: htmlContentType, wantBody: "Internal Server Error: error while generating and saving authcode\n", }, { - name: "state's internal version does not match what we want", - idp: happyUpstream().Build(), - method: http.MethodGet, - path: newRequestPath().WithState(happyUpstreamStateParam().WithStateVersion("wrong-state-version").Build(t, happyStateCodec)).String(), - csrfCookie: happyCSRFCookie, - wantStatus: http.StatusUnprocessableEntity, - wantBody: "Unprocessable Entity: state format version is invalid\n", + name: "state's internal version does not match what we want", + idp: happyUpstream().Build(), + method: http.MethodGet, + path: newRequestPath().WithState(happyUpstreamStateParam().WithStateVersion("wrong-state-version").Build(t, happyStateCodec)).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, + wantBody: "Unprocessable Entity: state format version is invalid\n", }, { name: "state's downstream auth params element is invalid", @@ -411,9 +412,10 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyUpstreamStateParam(). WithAuthorizeRequestParams("the following is an invalid url encoding token, and therefore this is an invalid param: %z"). Build(t, happyStateCodec)).String(), - csrfCookie: happyCSRFCookie, - wantStatus: http.StatusBadRequest, - wantBody: "Bad Request: error reading state downstream auth params\n", + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: error reading state downstream auth params\n", }, { name: "state's downstream auth params are missing required value (e.g., client_id)", @@ -424,9 +426,10 @@ func TestCallbackEndpoint(t *testing.T) { WithAuthorizeRequestParams(shallowCopyAndModifyQuery(happyDownstreamRequestParamsQuery, map[string]string{"client_id": ""}).Encode()). Build(t, happyStateCodec), ).String(), - csrfCookie: happyCSRFCookie, - wantStatus: http.StatusBadRequest, - wantBody: "Bad Request: error using state downstream auth params\n", + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusBadRequest, + wantContentType: htmlContentType, + wantBody: "Bad Request: error using state downstream auth params\n", }, { name: "state's downstream auth params does not contain openid scope", @@ -474,39 +477,43 @@ func TestCallbackEndpoint(t *testing.T) { wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, { - name: "the OIDCIdentityProvider CRD has been deleted", - idp: otherUpstreamOIDCIdentityProvider, - method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), - csrfCookie: happyCSRFCookie, - wantStatus: http.StatusUnprocessableEntity, - wantBody: "Unprocessable Entity: upstream provider not found\n", + name: "the OIDCIdentityProvider CRD has been deleted", + idp: otherUpstreamOIDCIdentityProvider, + method: http.MethodGet, + path: newRequestPath().WithState(happyState).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, + wantBody: "Unprocessable Entity: upstream provider not found\n", }, { - name: "the CSRF cookie does not exist on request", - idp: happyUpstream().Build(), - method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), - wantStatus: http.StatusForbidden, - wantBody: "Forbidden: CSRF cookie is missing\n", + name: "the CSRF cookie does not exist on request", + idp: happyUpstream().Build(), + method: http.MethodGet, + path: newRequestPath().WithState(happyState).String(), + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: CSRF cookie is missing\n", }, { - name: "cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason", - idp: happyUpstream().Build(), - method: http.MethodGet, - path: newRequestPath().WithState(happyState).String(), - csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped", - wantStatus: http.StatusForbidden, - wantBody: "Forbidden: error reading CSRF cookie\n", + name: "cookie was not signed correctly, has expired, or otherwise cannot be decoded for any reason", + idp: happyUpstream().Build(), + method: http.MethodGet, + path: newRequestPath().WithState(happyState).String(), + csrfCookie: "__Host-pinniped-csrf=this-value-was-not-signed-by-pinniped", + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: error reading CSRF cookie\n", }, { - name: "cookie csrf value does not match state csrf value", - idp: happyUpstream().Build(), - method: http.MethodGet, - path: newRequestPath().WithState(happyUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(), - csrfCookie: happyCSRFCookie, - wantStatus: http.StatusForbidden, - wantBody: "Forbidden: CSRF value does not match\n", + name: "cookie csrf value does not match state csrf value", + idp: happyUpstream().Build(), + method: http.MethodGet, + path: newRequestPath().WithState(happyUpstreamStateParam().WithCSRF("wrong-csrf-value").Build(t, happyStateCodec)).String(), + csrfCookie: happyCSRFCookie, + wantStatus: http.StatusForbidden, + wantContentType: htmlContentType, + wantBody: "Forbidden: CSRF value does not match\n", }, // Upstream exchange @@ -518,6 +525,7 @@ func TestCallbackEndpoint(t *testing.T) { csrfCookie: happyCSRFCookie, wantStatus: http.StatusBadGateway, wantBody: "Bad Gateway: error exchanging and validating upstream tokens\n", + wantContentType: htmlContentType, wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, { @@ -528,6 +536,7 @@ func TestCallbackEndpoint(t *testing.T) { csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, wantBody: "Unprocessable Entity: no username claim in upstream ID token\n", + wantContentType: htmlContentType, wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, { @@ -556,6 +565,7 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, wantBody: "Unprocessable Entity: username claim in upstream ID token has invalid format\n", wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, @@ -566,6 +576,7 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, wantBody: "Unprocessable Entity: issuer claim in upstream ID token missing\n", wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, @@ -576,6 +587,7 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, wantBody: "Unprocessable Entity: issuer claim in upstream ID token has invalid format\n", wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, @@ -586,6 +598,7 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, wantBody: "Unprocessable Entity: groups claim in upstream ID token has invalid format\n", wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, @@ -596,6 +609,7 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, wantBody: "Unprocessable Entity: groups claim in upstream ID token has invalid format\n", wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, @@ -606,6 +620,7 @@ func TestCallbackEndpoint(t *testing.T) { path: newRequestPath().WithState(happyState).String(), csrfCookie: happyCSRFCookie, wantStatus: http.StatusUnprocessableEntity, + wantContentType: htmlContentType, wantBody: "Unprocessable Entity: groups claim in upstream ID token has invalid format\n", wantExchangeAndValidateTokensCall: happyExchangeAndValidateTokensArgs, }, @@ -648,6 +663,7 @@ func TestCallbackEndpoint(t *testing.T) { } require.Equal(t, test.wantStatus, rsp.Code) + testutil.RequireEqualContentType(t, rsp.Header().Get("Content-Type"), test.wantContentType) if test.wantBody != "" { require.Equal(t, test.wantBody, rsp.Body.String()) @@ -656,79 +672,30 @@ func TestCallbackEndpoint(t *testing.T) { } if test.wantRedirectLocationRegexp != "" { //nolint:nestif // don't mind have several sequential if statements in this test - // Assert that Location header matches regular expression. require.Len(t, rsp.Header().Values("Location"), 1) - actualLocation := rsp.Header().Get("Location") - regex := regexp.MustCompile(test.wantRedirectLocationRegexp) - submatches := regex.FindStringSubmatch(actualLocation) - require.Lenf(t, submatches, 2, "no regexp match in actualLocation: %q", actualLocation) - capturedAuthCode := submatches[1] - - // fosite authcodes are in the format `data.signature`, so grab the signature part, which is the lookup key in the storage interface - authcodeDataAndSignature := strings.Split(capturedAuthCode, ".") - require.Len(t, authcodeDataAndSignature, 2) - - // Several Secrets should have been created - expectedNumberOfCreatedSecrets := 2 - if includesOpenIDScope(test.wantDownstreamGrantedScopes) { - expectedNumberOfCreatedSecrets++ - } - require.Len(t, client.Actions(), expectedNumberOfCreatedSecrets) - - // One authcode should have been stored. - testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) - - storedRequestFromAuthcode, storedSessionFromAuthcode := validateAuthcodeStorage( + oidctestutil.RequireAuthcodeRedirectLocation( t, + rsp.Header().Get("Location"), + test.wantRedirectLocationRegexp, + client, + secrets, oauthStore, - authcodeDataAndSignature[1], // Authcode store key is authcode signature test.wantDownstreamGrantedScopes, test.wantDownstreamIDTokenSubject, test.wantDownstreamIDTokenUsername, test.wantDownstreamIDTokenGroups, test.wantDownstreamRequestedScopes, - ) - - // One PKCE should have been stored. - testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: pkce.TypeLabelValue}, 1) - - validatePKCEStorage( - t, - oauthStore, - authcodeDataAndSignature[1], // PKCE store key is authcode signature - storedRequestFromAuthcode, - storedSessionFromAuthcode, test.wantDownstreamPKCEChallenge, test.wantDownstreamPKCEChallengeMethod, + test.wantDownstreamNonce, + downstreamClientID, + downstreamRedirectURI, ) - - // One IDSession should have been stored, if the downstream actually requested the "openid" scope - if includesOpenIDScope(test.wantDownstreamGrantedScopes) { - testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secrets, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1) - - validateIDSessionStorage( - t, - oauthStore, - capturedAuthCode, // IDSession store key is full authcode - storedRequestFromAuthcode, - storedSessionFromAuthcode, - test.wantDownstreamNonce, - ) - } } }) } } -func includesOpenIDScope(scopes []string) bool { - for _, scope := range scopes { - if scope == "openid" { - return true - } - } - return false -} - type requestPath struct { code, state *string } @@ -898,141 +865,3 @@ func shallowCopyAndModifyQuery(query url.Values, modifications map[string]string } return copied } - -func validateAuthcodeStorage( - t *testing.T, - oauthStore *oidc.KubeStorage, - storeKey string, - wantDownstreamGrantedScopes []string, - wantDownstreamIDTokenSubject string, - wantDownstreamIDTokenUsername string, - wantDownstreamIDTokenGroups []string, - wantDownstreamRequestedScopes []string, -) (*fosite.Request, *openid.DefaultSession) { - t.Helper() - - // Get the authcode session back from storage so we can require that it was stored correctly. - storedAuthorizeRequestFromAuthcode, err := oauthStore.GetAuthorizeCodeSession(context.Background(), storeKey, nil) - require.NoError(t, err) - - // Check that storage returned the expected concrete data types. - storedRequestFromAuthcode, storedSessionFromAuthcode := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromAuthcode) - - // Check which scopes were granted. - require.ElementsMatch(t, wantDownstreamGrantedScopes, storedRequestFromAuthcode.GetGrantedScopes()) - - // Check all the other fields of the stored request. - require.NotEmpty(t, storedRequestFromAuthcode.ID) - require.Equal(t, downstreamClientID, storedRequestFromAuthcode.Client.GetID()) - require.ElementsMatch(t, wantDownstreamRequestedScopes, storedRequestFromAuthcode.RequestedScope) - require.Nil(t, storedRequestFromAuthcode.RequestedAudience) - require.Empty(t, storedRequestFromAuthcode.GrantedAudience) - require.Equal(t, url.Values{"redirect_uri": []string{downstreamRedirectURI}}, storedRequestFromAuthcode.Form) - testutil.RequireTimeInDelta(t, time.Now(), storedRequestFromAuthcode.RequestedAt, timeComparisonFudgeFactor) - - // We're not using these fields yet, so confirm that we did not set them (for now). - require.Empty(t, storedSessionFromAuthcode.Subject) - require.Empty(t, storedSessionFromAuthcode.Username) - require.Empty(t, storedSessionFromAuthcode.Headers) - - // The authcode that we are issuing should be good for the length of time that we declare in the fosite config. - testutil.RequireTimeInDelta(t, time.Now().Add(authCodeExpirationSeconds*time.Second), storedSessionFromAuthcode.ExpiresAt[fosite.AuthorizeCode], timeComparisonFudgeFactor) - require.Len(t, storedSessionFromAuthcode.ExpiresAt, 1) - - // Now confirm the ID token claims. - actualClaims := storedSessionFromAuthcode.Claims - - // Check the user's identity, which are put into the downstream ID token's subject, username and groups claims. - require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject) - require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"]) - require.Len(t, actualClaims.Extra, 2) - actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] - require.NotNil(t, actualDownstreamIDTokenGroups) - require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualDownstreamIDTokenGroups) - - // Check the rest of the downstream ID token's claims. Fosite wants us to set these (in UTC time). - testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.RequestedAt, timeComparisonFudgeFactor) - testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.AuthTime, timeComparisonFudgeFactor) - requestedAtZone, _ := actualClaims.RequestedAt.Zone() - require.Equal(t, "UTC", requestedAtZone) - authTimeZone, _ := actualClaims.AuthTime.Zone() - require.Equal(t, "UTC", authTimeZone) - - // Fosite will set these fields for us in the token endpoint based on the store session - // information. Therefore, we assert that they are empty because we want the library to do the - // lifting for us. - require.Empty(t, actualClaims.Issuer) - require.Nil(t, actualClaims.Audience) - require.Empty(t, actualClaims.Nonce) - require.Zero(t, actualClaims.ExpiresAt) - require.Zero(t, actualClaims.IssuedAt) - - // These are not needed yet. - require.Empty(t, actualClaims.JTI) - require.Empty(t, actualClaims.CodeHash) - require.Empty(t, actualClaims.AccessTokenHash) - require.Empty(t, actualClaims.AuthenticationContextClassReference) - require.Empty(t, actualClaims.AuthenticationMethodsReference) - - return storedRequestFromAuthcode, storedSessionFromAuthcode -} - -func validatePKCEStorage( - t *testing.T, - oauthStore *oidc.KubeStorage, - storeKey string, - storedRequestFromAuthcode *fosite.Request, - storedSessionFromAuthcode *openid.DefaultSession, - wantDownstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod string, -) { - t.Helper() - - storedAuthorizeRequestFromPKCE, err := oauthStore.GetPKCERequestSession(context.Background(), storeKey, nil) - require.NoError(t, err) - - // Check that storage returned the expected concrete data types. - storedRequestFromPKCE, storedSessionFromPKCE := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromPKCE) - - // The stored PKCE request should be the same as the stored authcode request. - require.Equal(t, storedRequestFromAuthcode.ID, storedRequestFromPKCE.ID) - require.Equal(t, storedSessionFromAuthcode, storedSessionFromPKCE) - - // The stored PKCE request should also contain the PKCE challenge that the downstream sent us. - require.Equal(t, wantDownstreamPKCEChallenge, storedRequestFromPKCE.Form.Get("code_challenge")) - require.Equal(t, wantDownstreamPKCEChallengeMethod, storedRequestFromPKCE.Form.Get("code_challenge_method")) -} - -func validateIDSessionStorage( - t *testing.T, - oauthStore *oidc.KubeStorage, - storeKey string, - storedRequestFromAuthcode *fosite.Request, - storedSessionFromAuthcode *openid.DefaultSession, - wantDownstreamNonce string, -) { - t.Helper() - - storedAuthorizeRequestFromIDSession, err := oauthStore.GetOpenIDConnectSession(context.Background(), storeKey, nil) - require.NoError(t, err) - - // Check that storage returned the expected concrete data types. - storedRequestFromIDSession, storedSessionFromIDSession := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromIDSession) - - // The stored IDSession request should be the same as the stored authcode request. - require.Equal(t, storedRequestFromAuthcode.ID, storedRequestFromIDSession.ID) - require.Equal(t, storedSessionFromAuthcode, storedSessionFromIDSession) - - // The stored IDSession request should also contain the nonce that the downstream sent us. - require.Equal(t, wantDownstreamNonce, storedRequestFromIDSession.Form.Get("nonce")) -} - -func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requester) (*fosite.Request, *openid.DefaultSession) { - t.Helper() - - storedRequest, ok := storedAuthorizeRequest.(*fosite.Request) - require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest, &fosite.Request{}) - storedSession, ok := storedAuthorizeRequest.GetSession().(*openid.DefaultSession) - require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest.GetSession(), &openid.DefaultSession{}) - - return storedRequest, storedSession -} diff --git a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go b/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go index 6df5e5bc..5c32c798 100644 --- a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go +++ b/internal/oidc/dynamic_open_id_connect_ecdsa_strategy.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -8,14 +8,13 @@ import ( "crypto/ecdsa" "reflect" - "go.pinniped.dev/internal/constable" - "go.pinniped.dev/internal/plog" - "github.com/ory/fosite" "github.com/ory/fosite/compose" "github.com/ory/fosite/handler/openid" + "go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/oidc/jwks" + "go.pinniped.dev/internal/plog" ) // dynamicOpenIDConnectECDSAStrategy is an openid.OpenIDConnectTokenStrategy that can dynamically diff --git a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go b/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go index f0250e22..c8e036c1 100644 --- a/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go +++ b/internal/oidc/dynamic_open_id_connect_ecdsa_strategy_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -21,7 +21,7 @@ import ( "gopkg.in/square/go-jose.v2" "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/oidctestutil" + "go.pinniped.dev/internal/testutil/oidctestutil" ) func TestDynamicOpenIDConnectECDSAStrategy(t *testing.T) { diff --git a/internal/oidc/kube_storage.go b/internal/oidc/kube_storage.go index 46f9c947..f775c22b 100644 --- a/internal/oidc/kube_storage.go +++ b/internal/oidc/kube_storage.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -19,6 +19,7 @@ import ( "go.pinniped.dev/internal/fositestorage/openidconnect" "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestorage/refreshtoken" + "go.pinniped.dev/internal/fositestoragei" ) const errKubeStorageNotImplemented = constable.Error("KubeStorage does not implement this method. It should not have been called.") @@ -31,6 +32,8 @@ type KubeStorage struct { refreshTokenStorage refreshtoken.RevocationStorage } +var _ fositestoragei.AllFositeStorage = &KubeStorage{} + func NewKubeStorage(secrets corev1client.SecretInterface, timeoutsConfiguration TimeoutsConfiguration) *KubeStorage { nowFunc := time.Now return &KubeStorage{ diff --git a/internal/oidc/nullstorage.go b/internal/oidc/nullstorage.go index 3dcd7a06..121e3c3a 100644 --- a/internal/oidc/nullstorage.go +++ b/internal/oidc/nullstorage.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package oidc @@ -10,12 +10,15 @@ import ( "github.com/ory/fosite" "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/fositestoragei" ) const errNullStorageNotImplemented = constable.Error("NullStorage does not implement this method. It should not have been called.") type NullStorage struct{} +var _ fositestoragei.AllFositeStorage = &NullStorage{} + func (NullStorage) RevokeRefreshToken(_ context.Context, _ string) error { return errNullStorageNotImplemented } diff --git a/internal/oidc/oidctestutil/oidc.go b/internal/oidc/oidctestutil/oidc.go deleted file mode 100644 index 34938139..00000000 --- a/internal/oidc/oidctestutil/oidc.go +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package oidctestutil - -import ( - "context" - "crypto" - "crypto/ecdsa" - "fmt" - "net/url" - "testing" - - coreosoidc "github.com/coreos/go-oidc/v3/oidc" - "github.com/stretchr/testify/require" - "golang.org/x/oauth2" - "gopkg.in/square/go-jose.v2" - - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/pkg/oidcclient/nonce" - "go.pinniped.dev/pkg/oidcclient/oidctypes" - "go.pinniped.dev/pkg/oidcclient/pkce" -) - -// Test helpers for the OIDC package. - -// ExchangeAuthcodeAndValidateTokenArgs is a POGO (plain old go object?) used to spy on calls to -// TestUpstreamOIDCIdentityProvider.ExchangeAuthcodeAndValidateTokensFunc(). -type ExchangeAuthcodeAndValidateTokenArgs struct { - Ctx context.Context - Authcode string - PKCECodeVerifier pkce.Code - ExpectedIDTokenNonce nonce.Nonce - RedirectURI string -} - -type TestUpstreamOIDCIdentityProvider struct { - Name string - ClientID string - AuthorizationURL url.URL - UsernameClaim string - GroupsClaim string - Scopes []string - ExchangeAuthcodeAndValidateTokensFunc func( - ctx context.Context, - authcode string, - pkceCodeVerifier pkce.Code, - expectedIDTokenNonce nonce.Nonce, - ) (*oidctypes.Token, error) - - exchangeAuthcodeAndValidateTokensCallCount int - exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs -} - -func (u *TestUpstreamOIDCIdentityProvider) GetName() string { - return u.Name -} - -func (u *TestUpstreamOIDCIdentityProvider) GetClientID() string { - return u.ClientID -} - -func (u *TestUpstreamOIDCIdentityProvider) GetAuthorizationURL() *url.URL { - return &u.AuthorizationURL -} - -func (u *TestUpstreamOIDCIdentityProvider) GetScopes() []string { - return u.Scopes -} - -func (u *TestUpstreamOIDCIdentityProvider) GetUsernameClaim() string { - return u.UsernameClaim -} - -func (u *TestUpstreamOIDCIdentityProvider) GetGroupsClaim() string { - return u.GroupsClaim -} - -func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokens( - ctx context.Context, - authcode string, - pkceCodeVerifier pkce.Code, - expectedIDTokenNonce nonce.Nonce, - redirectURI string, -) (*oidctypes.Token, error) { - if u.exchangeAuthcodeAndValidateTokensArgs == nil { - u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0) - } - u.exchangeAuthcodeAndValidateTokensCallCount++ - u.exchangeAuthcodeAndValidateTokensArgs = append(u.exchangeAuthcodeAndValidateTokensArgs, &ExchangeAuthcodeAndValidateTokenArgs{ - Ctx: ctx, - Authcode: authcode, - PKCECodeVerifier: pkceCodeVerifier, - ExpectedIDTokenNonce: expectedIDTokenNonce, - RedirectURI: redirectURI, - }) - return u.ExchangeAuthcodeAndValidateTokensFunc(ctx, authcode, pkceCodeVerifier, expectedIDTokenNonce) -} - -func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensCallCount() int { - return u.exchangeAuthcodeAndValidateTokensCallCount -} - -func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensArgs(call int) *ExchangeAuthcodeAndValidateTokenArgs { - if u.exchangeAuthcodeAndValidateTokensArgs == nil { - u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0) - } - return u.exchangeAuthcodeAndValidateTokensArgs[call] -} - -func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(_ context.Context, _ *oauth2.Token, _ nonce.Nonce) (*oidctypes.Token, error) { - panic("implement me") -} - -type UpstreamIDPListerBuilder struct { - upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider -} - -func (b *UpstreamIDPListerBuilder) WithOIDC(upstreamOIDCIdentityProviders ...*TestUpstreamOIDCIdentityProvider) *UpstreamIDPListerBuilder { - b.upstreamOIDCIdentityProviders = append(b.upstreamOIDCIdentityProviders, upstreamOIDCIdentityProviders...) - return b -} - -func (b *UpstreamIDPListerBuilder) Build() provider.DynamicUpstreamIDPProvider { - idpProvider := provider.NewDynamicUpstreamIDPProvider() - upstreams := make([]provider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) - for i := range b.upstreamOIDCIdentityProviders { - upstreams[i] = provider.UpstreamOIDCIdentityProviderI(b.upstreamOIDCIdentityProviders[i]) - } - idpProvider.SetOIDCIdentityProviders(upstreams) - return idpProvider -} - -func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder { - return &UpstreamIDPListerBuilder{} -} - -// Declare a separate type from the production code to ensure that the state param's contents was serialized -// in the format that we expect, with the json keys that we expect, etc. This also ensure that the order of -// the serialized fields is the same, which doesn't really matter expect that we can make simpler equality -// assertions about the redirect URL in this test. -type ExpectedUpstreamStateParamFormat struct { - P string `json:"p"` - U string `json:"u"` - N string `json:"n"` - C string `json:"c"` - K string `json:"k"` - V string `json:"v"` -} - -type staticKeySet struct { - publicKey crypto.PublicKey -} - -func newStaticKeySet(publicKey crypto.PublicKey) coreosoidc.KeySet { - return &staticKeySet{publicKey} -} - -func (s *staticKeySet) VerifySignature(ctx context.Context, jwt string) ([]byte, error) { - jws, err := jose.ParseSigned(jwt) - if err != nil { - return nil, fmt.Errorf("oidc: malformed jwt: %w", err) - } - return jws.Verify(s.publicKey) -} - -// VerifyECDSAIDToken verifies that the provided idToken was issued via the provided jwtSigningKey. -// It also performs some light validation on the claims, i.e., it makes sure the provided idToken -// has the provided issuer and clientID. -// -// Further validation can be done via callers via the returned coreosoidc.IDToken. -func VerifyECDSAIDToken( - t *testing.T, - issuer, clientID string, - jwtSigningKey *ecdsa.PrivateKey, - idToken string, -) *coreosoidc.IDToken { - t.Helper() - - keySet := newStaticKeySet(jwtSigningKey.Public()) - verifyConfig := coreosoidc.Config{ClientID: clientID, SupportedSigningAlgs: []string{coreosoidc.ES256}} - verifier := coreosoidc.NewVerifier(issuer, keySet, &verifyConfig) - token, err := verifier.Verify(context.Background(), idToken) - require.NoError(t, err) - - return token -} diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index 1893352b..59175fac 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -53,6 +53,11 @@ type UpstreamLDAPIdentityProviderI interface { // A name for this upstream provider. GetName() string + // Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234". + // This URL is not used for connecting to the provider, but rather is used for creating a globally unique user + // identifier by being combined with the user's UID, since user UIDs are only unique within one provider. + GetURL() string + // A method for performing user authentication against the upstream LDAP provider. ldap.UserAuthenticator } diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index a5d79df9..04f30fa0 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -25,9 +25,9 @@ import ( "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/discovery" "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/oidctestutil" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/oidctestutil" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/pkce" diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index 6bc1ad63..dee77cd8 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package token @@ -40,11 +40,12 @@ import ( "go.pinniped.dev/internal/fositestorage/openidconnect" storagepkce "go.pinniped.dev/internal/fositestorage/pkce" "go.pinniped.dev/internal/fositestorage/refreshtoken" + "go.pinniped.dev/internal/fositestoragei" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/oidc/jwks" - "go.pinniped.dev/internal/oidc/oidctestutil" "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/oidctestutil" ) const ( @@ -214,25 +215,13 @@ type authcodeExchangeInputs struct { modifyTokenRequest func(tokenRequest *http.Request, authCode string) modifyStorage func( t *testing.T, - s interface { - oauth2.TokenRevocationStorage - oauth2.CoreStorage - openid.OpenIDConnectRequestStorage - pkce.PKCERequestStorage - fosite.ClientManager - }, + s fositestoragei.AllFositeStorage, authCode string, ) makeOathHelper func( t *testing.T, authRequest *http.Request, - store interface { - oauth2.TokenRevocationStorage - oauth2.CoreStorage - openid.OpenIDConnectRequestStorage - pkce.PKCERequestStorage - fosite.ClientManager - }, + store fositestoragei.AllFositeStorage, ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) want tokenEndpointResponseExpectedValues @@ -1315,13 +1304,7 @@ func getFositeDataSignature(t *testing.T, data string) string { func makeHappyOauthHelper( t *testing.T, authRequest *http.Request, - store interface { - oauth2.TokenRevocationStorage - oauth2.CoreStorage - openid.OpenIDConnectRequestStorage - pkce.PKCERequestStorage - fosite.ClientManager - }, + store fositestoragei.AllFositeStorage, ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { t.Helper() @@ -1347,13 +1330,7 @@ func (s *singleUseJWKProvider) GetJWKS(issuerName string) (jwks *jose.JSONWebKey func makeOauthHelperWithJWTKeyThatWorksOnlyOnce( t *testing.T, authRequest *http.Request, - store interface { - oauth2.TokenRevocationStorage - oauth2.CoreStorage - openid.OpenIDConnectRequestStorage - pkce.PKCERequestStorage - fosite.ClientManager - }, + store fositestoragei.AllFositeStorage, ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { t.Helper() @@ -1366,13 +1343,7 @@ func makeOauthHelperWithJWTKeyThatWorksOnlyOnce( func makeOauthHelperWithNilPrivateJWTSigningKey( t *testing.T, authRequest *http.Request, - store interface { - oauth2.TokenRevocationStorage - oauth2.CoreStorage - openid.OpenIDConnectRequestStorage - pkce.PKCERequestStorage - fosite.ClientManager - }, + store fositestoragei.AllFositeStorage, ) (fosite.OAuth2Provider, string, *ecdsa.PrivateKey) { t.Helper() diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go new file mode 100644 index 00000000..fdf998d1 --- /dev/null +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -0,0 +1,469 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidctestutil + +import ( + "context" + "crypto" + "crypto/ecdsa" + "fmt" + "net/url" + "regexp" + "strings" + "testing" + "time" + + coreosoidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/ory/fosite" + "github.com/ory/fosite/handler/openid" + "github.com/stretchr/testify/require" + "golang.org/x/oauth2" + "gopkg.in/square/go-jose.v2" + "k8s.io/apimachinery/pkg/labels" + "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/crud" + "go.pinniped.dev/internal/fositestorage/authorizationcode" + "go.pinniped.dev/internal/fositestorage/openidconnect" + pkce2 "go.pinniped.dev/internal/fositestorage/pkce" + "go.pinniped.dev/internal/fositestoragei" + "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/pkg/oidcclient/nonce" + "go.pinniped.dev/pkg/oidcclient/oidctypes" + "go.pinniped.dev/pkg/oidcclient/pkce" +) + +// Test helpers for the OIDC package. + +// ExchangeAuthcodeAndValidateTokenArgs is a POGO (plain old go object?) used to spy on calls to +// TestUpstreamOIDCIdentityProvider.ExchangeAuthcodeAndValidateTokensFunc(). +type ExchangeAuthcodeAndValidateTokenArgs struct { + Ctx context.Context + Authcode string + PKCECodeVerifier pkce.Code + ExpectedIDTokenNonce nonce.Nonce + RedirectURI string +} + +type TestUpstreamLDAPIdentityProvider struct { + Name string + URL string + AuthenticateFunc func(ctx context.Context, username, password string) (*authenticator.Response, bool, error) +} + +func (u *TestUpstreamLDAPIdentityProvider) GetName() string { + return u.Name +} + +func (u *TestUpstreamLDAPIdentityProvider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { + return u.AuthenticateFunc(ctx, username, password) +} + +func (u *TestUpstreamLDAPIdentityProvider) GetURL() string { + return u.URL +} + +type TestUpstreamOIDCIdentityProvider struct { + Name string + ClientID string + AuthorizationURL url.URL + UsernameClaim string + GroupsClaim string + Scopes []string + ExchangeAuthcodeAndValidateTokensFunc func( + ctx context.Context, + authcode string, + pkceCodeVerifier pkce.Code, + expectedIDTokenNonce nonce.Nonce, + ) (*oidctypes.Token, error) + + exchangeAuthcodeAndValidateTokensCallCount int + exchangeAuthcodeAndValidateTokensArgs []*ExchangeAuthcodeAndValidateTokenArgs +} + +func (u *TestUpstreamOIDCIdentityProvider) GetName() string { + return u.Name +} + +func (u *TestUpstreamOIDCIdentityProvider) GetClientID() string { + return u.ClientID +} + +func (u *TestUpstreamOIDCIdentityProvider) GetAuthorizationURL() *url.URL { + return &u.AuthorizationURL +} + +func (u *TestUpstreamOIDCIdentityProvider) GetScopes() []string { + return u.Scopes +} + +func (u *TestUpstreamOIDCIdentityProvider) GetUsernameClaim() string { + return u.UsernameClaim +} + +func (u *TestUpstreamOIDCIdentityProvider) GetGroupsClaim() string { + return u.GroupsClaim +} + +func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokens( + ctx context.Context, + authcode string, + pkceCodeVerifier pkce.Code, + expectedIDTokenNonce nonce.Nonce, + redirectURI string, +) (*oidctypes.Token, error) { + if u.exchangeAuthcodeAndValidateTokensArgs == nil { + u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0) + } + u.exchangeAuthcodeAndValidateTokensCallCount++ + u.exchangeAuthcodeAndValidateTokensArgs = append(u.exchangeAuthcodeAndValidateTokensArgs, &ExchangeAuthcodeAndValidateTokenArgs{ + Ctx: ctx, + Authcode: authcode, + PKCECodeVerifier: pkceCodeVerifier, + ExpectedIDTokenNonce: expectedIDTokenNonce, + RedirectURI: redirectURI, + }) + return u.ExchangeAuthcodeAndValidateTokensFunc(ctx, authcode, pkceCodeVerifier, expectedIDTokenNonce) +} + +func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensCallCount() int { + return u.exchangeAuthcodeAndValidateTokensCallCount +} + +func (u *TestUpstreamOIDCIdentityProvider) ExchangeAuthcodeAndValidateTokensArgs(call int) *ExchangeAuthcodeAndValidateTokenArgs { + if u.exchangeAuthcodeAndValidateTokensArgs == nil { + u.exchangeAuthcodeAndValidateTokensArgs = make([]*ExchangeAuthcodeAndValidateTokenArgs, 0) + } + return u.exchangeAuthcodeAndValidateTokensArgs[call] +} + +func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(_ context.Context, _ *oauth2.Token, _ nonce.Nonce) (*oidctypes.Token, error) { + panic("implement me") +} + +type UpstreamIDPListerBuilder struct { + upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider + upstreamLDAPIdentityProviders []*TestUpstreamLDAPIdentityProvider +} + +func (b *UpstreamIDPListerBuilder) WithOIDC(upstreamOIDCIdentityProviders ...*TestUpstreamOIDCIdentityProvider) *UpstreamIDPListerBuilder { + b.upstreamOIDCIdentityProviders = append(b.upstreamOIDCIdentityProviders, upstreamOIDCIdentityProviders...) + return b +} + +func (b *UpstreamIDPListerBuilder) WithLDAP(upstreamLDAPIdentityProviders ...*TestUpstreamLDAPIdentityProvider) *UpstreamIDPListerBuilder { + b.upstreamLDAPIdentityProviders = append(b.upstreamLDAPIdentityProviders, upstreamLDAPIdentityProviders...) + return b +} + +func (b *UpstreamIDPListerBuilder) Build() provider.DynamicUpstreamIDPProvider { + idpProvider := provider.NewDynamicUpstreamIDPProvider() + + oidcUpstreams := make([]provider.UpstreamOIDCIdentityProviderI, len(b.upstreamOIDCIdentityProviders)) + for i := range b.upstreamOIDCIdentityProviders { + oidcUpstreams[i] = provider.UpstreamOIDCIdentityProviderI(b.upstreamOIDCIdentityProviders[i]) + } + idpProvider.SetOIDCIdentityProviders(oidcUpstreams) + + ldapUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, len(b.upstreamLDAPIdentityProviders)) + for i := range b.upstreamLDAPIdentityProviders { + ldapUpstreams[i] = provider.UpstreamLDAPIdentityProviderI(b.upstreamLDAPIdentityProviders[i]) + } + idpProvider.SetLDAPIdentityProviders(ldapUpstreams) + + return idpProvider +} + +func NewUpstreamIDPListerBuilder() *UpstreamIDPListerBuilder { + return &UpstreamIDPListerBuilder{} +} + +// Declare a separate type from the production code to ensure that the state param's contents was serialized +// in the format that we expect, with the json keys that we expect, etc. This also ensure that the order of +// the serialized fields is the same, which doesn't really matter expect that we can make simpler equality +// assertions about the redirect URL in this test. +type ExpectedUpstreamStateParamFormat struct { + P string `json:"p"` + U string `json:"u"` + N string `json:"n"` + C string `json:"c"` + K string `json:"k"` + V string `json:"v"` +} + +type staticKeySet struct { + publicKey crypto.PublicKey +} + +func newStaticKeySet(publicKey crypto.PublicKey) coreosoidc.KeySet { + return &staticKeySet{publicKey} +} + +func (s *staticKeySet) VerifySignature(_ context.Context, jwt string) ([]byte, error) { + jws, err := jose.ParseSigned(jwt) + if err != nil { + return nil, fmt.Errorf("oidc: malformed jwt: %w", err) + } + return jws.Verify(s.publicKey) +} + +// VerifyECDSAIDToken verifies that the provided idToken was issued via the provided jwtSigningKey. +// It also performs some light validation on the claims, i.e., it makes sure the provided idToken +// has the provided issuer and clientID. +// +// Further validation can be done via callers via the returned coreosoidc.IDToken. +func VerifyECDSAIDToken( + t *testing.T, + issuer, clientID string, + jwtSigningKey *ecdsa.PrivateKey, + idToken string, +) *coreosoidc.IDToken { + t.Helper() + + keySet := newStaticKeySet(jwtSigningKey.Public()) + verifyConfig := coreosoidc.Config{ClientID: clientID, SupportedSigningAlgs: []string{coreosoidc.ES256}} + verifier := coreosoidc.NewVerifier(issuer, keySet, &verifyConfig) + token, err := verifier.Verify(context.Background(), idToken) + require.NoError(t, err) + + return token +} + +func RequireAuthcodeRedirectLocation( + t *testing.T, + actualRedirectLocation string, + wantRedirectLocationRegexp string, + kubeClient *fake.Clientset, + secretsClient v1.SecretInterface, + oauthStore fositestoragei.AllFositeStorage, + wantDownstreamGrantedScopes []string, + wantDownstreamIDTokenSubject string, + wantDownstreamIDTokenUsername string, + wantDownstreamIDTokenGroups []string, + wantDownstreamRequestedScopes []string, + wantDownstreamPKCEChallenge string, + wantDownstreamPKCEChallengeMethod string, + wantDownstreamNonce string, + wantDownstreamClientID string, + wantDownstreamRedirectURI string, +) { + t.Helper() + + // Assert that Location header matches regular expression. + regex := regexp.MustCompile(wantRedirectLocationRegexp) + submatches := regex.FindStringSubmatch(actualRedirectLocation) + require.Lenf(t, submatches, 2, "no regexp match in actualRedirectLocation: %q", actualRedirectLocation) + capturedAuthCode := submatches[1] + + // fosite authcodes are in the format `data.signature`, so grab the signature part, which is the lookup key in the storage interface + authcodeDataAndSignature := strings.Split(capturedAuthCode, ".") + require.Len(t, authcodeDataAndSignature, 2) + + // Several Secrets should have been created + expectedNumberOfCreatedSecrets := 2 + if includesOpenIDScope(wantDownstreamGrantedScopes) { + expectedNumberOfCreatedSecrets++ + } + require.Len(t, kubeClient.Actions(), expectedNumberOfCreatedSecrets) + + // One authcode should have been stored. + testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: authorizationcode.TypeLabelValue}, 1) + + storedRequestFromAuthcode, storedSessionFromAuthcode := validateAuthcodeStorage( + t, + oauthStore, + authcodeDataAndSignature[1], // Authcode store key is authcode signature + wantDownstreamGrantedScopes, + wantDownstreamIDTokenSubject, + wantDownstreamIDTokenUsername, + wantDownstreamIDTokenGroups, + wantDownstreamRequestedScopes, + wantDownstreamClientID, + wantDownstreamRedirectURI, + ) + + // One PKCE should have been stored. + testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: pkce2.TypeLabelValue}, 1) + + validatePKCEStorage( + t, + oauthStore, + authcodeDataAndSignature[1], // PKCE store key is authcode signature + storedRequestFromAuthcode, + storedSessionFromAuthcode, + wantDownstreamPKCEChallenge, + wantDownstreamPKCEChallengeMethod, + ) + + // One IDSession should have been stored, if the downstream actually requested the "openid" scope + if includesOpenIDScope(wantDownstreamGrantedScopes) { + testutil.RequireNumberOfSecretsMatchingLabelSelector(t, secretsClient, labels.Set{crud.SecretLabelKey: openidconnect.TypeLabelValue}, 1) + + validateIDSessionStorage( + t, + oauthStore, + capturedAuthCode, // IDSession store key is full authcode + storedRequestFromAuthcode, + storedSessionFromAuthcode, + wantDownstreamNonce, + ) + } +} + +func includesOpenIDScope(scopes []string) bool { + for _, scope := range scopes { + if scope == "openid" { + return true + } + } + return false +} + +func validateAuthcodeStorage( + t *testing.T, + oauthStore fositestoragei.AllFositeStorage, + storeKey string, + wantDownstreamGrantedScopes []string, + wantDownstreamIDTokenSubject string, + wantDownstreamIDTokenUsername string, + wantDownstreamIDTokenGroups []string, + wantDownstreamRequestedScopes []string, + wantDownstreamClientID string, + wantDownstreamRedirectURI string, +) (*fosite.Request, *openid.DefaultSession) { + t.Helper() + + const ( + authCodeExpirationSeconds = 10 * 60 // Currently, we set our auth code expiration to 10 minutes + timeComparisonFudgeFactor = time.Second * 15 + ) + + // Get the authcode session back from storage so we can require that it was stored correctly. + storedAuthorizeRequestFromAuthcode, err := oauthStore.GetAuthorizeCodeSession(context.Background(), storeKey, nil) + require.NoError(t, err) + + // Check that storage returned the expected concrete data types. + storedRequestFromAuthcode, storedSessionFromAuthcode := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromAuthcode) + + // Check which scopes were granted. + require.ElementsMatch(t, wantDownstreamGrantedScopes, storedRequestFromAuthcode.GetGrantedScopes()) + + // Check all the other fields of the stored request. + require.NotEmpty(t, storedRequestFromAuthcode.ID) + require.Equal(t, wantDownstreamClientID, storedRequestFromAuthcode.Client.GetID()) + require.ElementsMatch(t, wantDownstreamRequestedScopes, storedRequestFromAuthcode.RequestedScope) + require.Nil(t, storedRequestFromAuthcode.RequestedAudience) + require.Empty(t, storedRequestFromAuthcode.GrantedAudience) + require.Equal(t, url.Values{"redirect_uri": []string{wantDownstreamRedirectURI}}, storedRequestFromAuthcode.Form) + testutil.RequireTimeInDelta(t, time.Now(), storedRequestFromAuthcode.RequestedAt, timeComparisonFudgeFactor) + + // We're not using these fields yet, so confirm that we did not set them (for now). + require.Empty(t, storedSessionFromAuthcode.Subject) + require.Empty(t, storedSessionFromAuthcode.Username) + require.Empty(t, storedSessionFromAuthcode.Headers) + + // The authcode that we are issuing should be good for the length of time that we declare in the fosite config. + testutil.RequireTimeInDelta(t, time.Now().Add(authCodeExpirationSeconds*time.Second), storedSessionFromAuthcode.ExpiresAt[fosite.AuthorizeCode], timeComparisonFudgeFactor) + require.Len(t, storedSessionFromAuthcode.ExpiresAt, 1) + + // Now confirm the ID token claims. + actualClaims := storedSessionFromAuthcode.Claims + + // Check the user's identity, which are put into the downstream ID token's subject, username and groups claims. + require.Equal(t, wantDownstreamIDTokenSubject, actualClaims.Subject) + require.Equal(t, wantDownstreamIDTokenUsername, actualClaims.Extra["username"]) + require.Len(t, actualClaims.Extra, 2) + actualDownstreamIDTokenGroups := actualClaims.Extra["groups"] + require.NotNil(t, actualDownstreamIDTokenGroups) + require.ElementsMatch(t, wantDownstreamIDTokenGroups, actualDownstreamIDTokenGroups) + + // Check the rest of the downstream ID token's claims. Fosite wants us to set these (in UTC time). + testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.RequestedAt, timeComparisonFudgeFactor) + testutil.RequireTimeInDelta(t, time.Now().UTC(), actualClaims.AuthTime, timeComparisonFudgeFactor) + requestedAtZone, _ := actualClaims.RequestedAt.Zone() + require.Equal(t, "UTC", requestedAtZone) + authTimeZone, _ := actualClaims.AuthTime.Zone() + require.Equal(t, "UTC", authTimeZone) + + // Fosite will set these fields for us in the token endpoint based on the store session + // information. Therefore, we assert that they are empty because we want the library to do the + // lifting for us. + require.Empty(t, actualClaims.Issuer) + require.Nil(t, actualClaims.Audience) + require.Empty(t, actualClaims.Nonce) + require.Zero(t, actualClaims.ExpiresAt) + require.Zero(t, actualClaims.IssuedAt) + + // These are not needed yet. + require.Empty(t, actualClaims.JTI) + require.Empty(t, actualClaims.CodeHash) + require.Empty(t, actualClaims.AccessTokenHash) + require.Empty(t, actualClaims.AuthenticationContextClassReference) + require.Empty(t, actualClaims.AuthenticationMethodsReference) + + return storedRequestFromAuthcode, storedSessionFromAuthcode +} + +func validatePKCEStorage( + t *testing.T, + oauthStore fositestoragei.AllFositeStorage, + storeKey string, + storedRequestFromAuthcode *fosite.Request, + storedSessionFromAuthcode *openid.DefaultSession, + wantDownstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod string, +) { + t.Helper() + + storedAuthorizeRequestFromPKCE, err := oauthStore.GetPKCERequestSession(context.Background(), storeKey, nil) + require.NoError(t, err) + + // Check that storage returned the expected concrete data types. + storedRequestFromPKCE, storedSessionFromPKCE := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromPKCE) + + // The stored PKCE request should be the same as the stored authcode request. + require.Equal(t, storedRequestFromAuthcode.ID, storedRequestFromPKCE.ID) + require.Equal(t, storedSessionFromAuthcode, storedSessionFromPKCE) + + // The stored PKCE request should also contain the PKCE challenge that the downstream sent us. + require.Equal(t, wantDownstreamPKCEChallenge, storedRequestFromPKCE.Form.Get("code_challenge")) + require.Equal(t, wantDownstreamPKCEChallengeMethod, storedRequestFromPKCE.Form.Get("code_challenge_method")) +} + +func validateIDSessionStorage( + t *testing.T, + oauthStore fositestoragei.AllFositeStorage, + storeKey string, + storedRequestFromAuthcode *fosite.Request, + storedSessionFromAuthcode *openid.DefaultSession, + wantDownstreamNonce string, +) { + t.Helper() + + storedAuthorizeRequestFromIDSession, err := oauthStore.GetOpenIDConnectSession(context.Background(), storeKey, nil) + require.NoError(t, err) + + // Check that storage returned the expected concrete data types. + storedRequestFromIDSession, storedSessionFromIDSession := castStoredAuthorizeRequest(t, storedAuthorizeRequestFromIDSession) + + // The stored IDSession request should be the same as the stored authcode request. + require.Equal(t, storedRequestFromAuthcode.ID, storedRequestFromIDSession.ID) + require.Equal(t, storedSessionFromAuthcode, storedSessionFromIDSession) + + // The stored IDSession request should also contain the nonce that the downstream sent us. + require.Equal(t, wantDownstreamNonce, storedRequestFromIDSession.Form.Get("nonce")) +} + +func castStoredAuthorizeRequest(t *testing.T, storedAuthorizeRequest fosite.Requester) (*fosite.Request, *openid.DefaultSession) { + t.Helper() + + storedRequest, ok := storedAuthorizeRequest.(*fosite.Request) + require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest, &fosite.Request{}) + storedSession, ok := storedAuthorizeRequest.GetSession().(*openid.DefaultSession) + require.Truef(t, ok, "could not cast %T to %T", storedAuthorizeRequest.GetSession(), &openid.DefaultSession{}) + + return storedRequest, storedSession +} From 4ab704b7de7493325b23e595c2dd0c99e11139c7 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Fri, 9 Apr 2021 11:38:53 -0400 Subject: [PATCH 07/59] ldap: add initial stub upstream LDAP connection package Signed-off-by: Andrew Keesler --- go.mod | 1 + go.sum | 7 ++ internal/mocks/mockldapconn/generate.go | 6 ++ internal/mocks/mockldapconn/mockldapconn.go | 80 +++++++++++++++ internal/upstreamldap/upstreamldap.go | 70 +++++++++++++ internal/upstreamldap/upstreamldap_test.go | 104 ++++++++++++++++++++ 6 files changed, 268 insertions(+) create mode 100644 internal/mocks/mockldapconn/generate.go create mode 100644 internal/mocks/mockldapconn/mockldapconn.go create mode 100644 internal/upstreamldap/upstreamldap.go create mode 100644 internal/upstreamldap/upstreamldap_test.go diff --git a/go.mod b/go.mod index 9541d8da..550cb6dd 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/coreos/go-oidc/v3 v3.0.0 github.com/davecgh/go-spew v1.1.1 + github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-logr/logr v0.4.0 github.com/go-logr/stdr v0.4.0 github.com/go-openapi/spec v0.20.3 diff --git a/go.sum b/go.sum index 919c1ecc..50e700d8 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8 github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= +github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= @@ -201,12 +203,16 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= +github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-ldap/ldap/v3 v3.3.0 h1:lwx+SJpgOHd8tG6SumBQZXCmNX51zM8B1cfxJ5gv4tQ= +github.com/go-ldap/ldap/v3 v3.3.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= @@ -1094,6 +1100,7 @@ golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200320181102-891825fb96df/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= diff --git a/internal/mocks/mockldapconn/generate.go b/internal/mocks/mockldapconn/generate.go new file mode 100644 index 00000000..e9bf5943 --- /dev/null +++ b/internal/mocks/mockldapconn/generate.go @@ -0,0 +1,6 @@ +// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package mockldapconn + +//go:generate go run -v github.com/golang/mock/mockgen -destination=mockldapconn.go -package=mockldapconn -copyright_file=../../../hack/header.txt go.pinniped.dev/internal/upstreamldap Conn diff --git a/internal/mocks/mockldapconn/mockldapconn.go b/internal/mocks/mockldapconn/mockldapconn.go new file mode 100644 index 00000000..a96cf79c --- /dev/null +++ b/internal/mocks/mockldapconn/mockldapconn.go @@ -0,0 +1,80 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: go.pinniped.dev/internal/upstreamldap (interfaces: Conn) + +// Package mockldapconn is a generated GoMock package. +package mockldapconn + +import ( + reflect "reflect" + + ldap "github.com/go-ldap/ldap/v3" + gomock "github.com/golang/mock/gomock" +) + +// MockConn is a mock of Conn interface. +type MockConn struct { + ctrl *gomock.Controller + recorder *MockConnMockRecorder +} + +// MockConnMockRecorder is the mock recorder for MockConn. +type MockConnMockRecorder struct { + mock *MockConn +} + +// NewMockConn creates a new mock instance. +func NewMockConn(ctrl *gomock.Controller) *MockConn { + mock := &MockConn{ctrl: ctrl} + mock.recorder = &MockConnMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConn) EXPECT() *MockConnMockRecorder { + return m.recorder +} + +// Bind mocks base method. +func (m *MockConn) Bind(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Bind", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Bind indicates an expected call of Bind. +func (mr *MockConnMockRecorder) Bind(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bind", reflect.TypeOf((*MockConn)(nil).Bind), arg0, arg1) +} + +// Close mocks base method. +func (m *MockConn) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockConnMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConn)(nil).Close)) +} + +// Search mocks base method. +func (m *MockConn) Search(arg0 *ldap.SearchRequest) (*ldap.SearchResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Search", arg0) + ret0, _ := ret[0].(*ldap.SearchResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Search indicates an expected call of Search. +func (mr *MockConnMockRecorder) Search(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockConn)(nil).Search), arg0) +} diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go new file mode 100644 index 00000000..bc8a7eee --- /dev/null +++ b/internal/upstreamldap/upstreamldap.go @@ -0,0 +1,70 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package upstreamldap implements an abstraction of upstream LDAP IDP interactions. +package upstreamldap + +import ( + "context" + + ldap "github.com/go-ldap/ldap/v3" + "k8s.io/apiserver/pkg/authentication/authenticator" +) + +// Conn abstracts the upstream LDAP communication protocol (mostly for testing). +type Conn interface { + // Bind abstracts ldap.Conn.Bind(). + Bind(username, password string) error + // Search abstracts ldap.Conn.Search(). + Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) + // Close abstracts ldap.Conn.Close(). + Close() +} + +// UserSearch contains information about how to search for users in the upstream LDAP IDP. +type UserSearch struct { + // Base is the base DN to use for the user search in the upstream LDAP IDP. + Base string + // Filter is the filter to use for the user search in the upstream LDAP IDP. + Filter string + // UsernameAttribute is the attribute in the LDAP entry from which the username should be + // retrieved. + UsernameAttribute string + // UIDAttribute is the attribute in the LDAP entry from which the user's unique ID should be + // retrieved. + UIDAttribute string +} + +// Provider contains can interact with an upstream LDAP IDP. +type Provider struct { + // Name is the unique name of this upstream LDAP IDP. + Name string + // URL is the URL of this upstream LDAP IDP. + URL string + + // Dial is a func that, given a URL, will return an LDAPConn to use for communicating with an + // upstream LDAP IDP. + Dial func(ctx context.Context, url string) (Conn, error) + + // BindUsername is the username to use when performing a bind with the upstream LDAP IDP. + BindUsername string + // BindPassword is the password to use when performing a bind with the upstream LDAP IDP. + BindPassword string + + // UserSearch contains information about how to search for users in the upstream LDAP IDP. + UserSearch *UserSearch +} + +func (p *Provider) GetName() string { + return p.Name +} + +func (p *Provider) GetURL() string { + return p.URL +} + +func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { + // TODO: test context timeout? + // TODO: test dial context timeout? + return nil, false, nil +} diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go new file mode 100644 index 00000000..7f739144 --- /dev/null +++ b/internal/upstreamldap/upstreamldap_test.go @@ -0,0 +1,104 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package upstreamldap + +import ( + "context" + "testing" + + ldap "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/mocks/mockldapconn" +) + +var ( + upstreamUsername = "some-upstream-username" + upstreamPassword = "some-upstream-password" + upstreamGroups = []string{"some-upstream-group-0", "some-upstream-group-1"} + upstreamUID = "some-upstream-uid" +) + +func TestAuthenticateUser(t *testing.T) { + // Please the linter... + _ = upstreamGroups + _ = upstreamUID + t.Skip("TODO: make me pass!") + + tests := []struct { + name string + provider *Provider + wantError string + wantUnauthenticated bool + wantAuthResponse *authenticator.Response + }{ + { + name: "happy path", + provider: &Provider{ + URL: "ldaps://some-ldap-url:1234", + BindUsername: upstreamUsername, + BindPassword: upstreamPassword, + UserSearch: &UserSearch{ + Base: "some-upstream-base-dn", + Filter: "some-filter", + UsernameAttribute: "some-upstream-username-attribute", + UIDAttribute: "some-upstream-uid-attribute", + }, + }, + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: upstreamUsername, + Groups: upstreamGroups, + UID: upstreamUID, + }, + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + conn := mockldapconn.NewMockConn(ctrl) + conn.EXPECT().Bind(test.provider.BindUsername, test.provider.BindPassword).Times(1) + conn.EXPECT().Search(&ldap.SearchRequest{ + BaseDN: test.provider.UserSearch.Base, + Scope: 99, // TODO: what should this be? + DerefAliases: 99, // TODO: what should this be? + SizeLimit: 99, + TimeLimit: 99, // TODO: what should this be? + TypesOnly: true, // TODO: what should this be? + Filter: test.provider.UserSearch.Filter, + Attributes: []string{}, // TODO: what should this be? + Controls: []ldap.Control{}, // TODO: what should this be? + }).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", // TODO: what should this be? + Attributes: []*ldap.EntryAttribute{}, // TODO: what should this be? + }, + }, + Referrals: []string{}, // TODO: what should this be? + Controls: []ldap.Control{}, // TODO: what should this be? + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + + test.provider.Dial = func(ctx context.Context, url string) (Conn, error) { + require.Equal(t, test.provider.URL, url) + return conn, nil + } + + authResponse, authenticated, err := test.provider.AuthenticateUser(context.Background(), upstreamUsername, upstreamPassword) + if test.wantError != "" { + require.EqualError(t, err, test.wantError) + return + } + require.Equal(t, !test.wantUnauthenticated, authenticated) + require.Equal(t, test.wantAuthResponse, authResponse) + }) + } +} From 7781a2e17a995eb2b7c5883dbb795385d41cea6d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 9 Apr 2021 08:43:09 -0700 Subject: [PATCH 08/59] Some renames in pkg upstreamwatcher to make room for a second controller --- cmd/pinniped-supervisor/main.go | 2 +- ...eamwatcher.go => oidc_upstream_watcher.go} | 66 +++++++++---------- ..._test.go => oidc_upstream_watcher_test.go} | 60 ++++++++--------- 3 files changed, 64 insertions(+), 64 deletions(-) rename internal/controller/supervisorconfig/upstreamwatcher/{upstreamwatcher.go => oidc_upstream_watcher.go} (83%) rename internal/controller/supervisorconfig/upstreamwatcher/{upstreamwatcher_test.go => oidc_upstream_watcher_test.go} (78%) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 1f23e672..65ec1c13 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -233,7 +233,7 @@ func startControllers( singletonWorker, ). WithController( - upstreamwatcher.New( + upstreamwatcher.NewOIDCUpstreamWatcherController( dynamicUpstreamIDPProvider, pinnipedClient, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), diff --git a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go similarity index 83% rename from internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go rename to internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go index a4cc821e..cbf2690f 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go @@ -1,7 +1,7 @@ // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package upstreamwatcher implements a controller that watches OIDCIdentityProvider objects. +// Package upstreamwatcher implements controllers that watch the idp.supervisor.pinniped.dev API group's objects. package upstreamwatcher import ( @@ -37,7 +37,7 @@ import ( const ( // Setup for the name of our controller in logs. - controllerName = "upstream-observer" + oidcControllerName = "oidc-upstream-observer" // Constants related to the client credentials Secret. oidcClientSecretType corev1.SecretType = "secrets.pinniped.dev/oidc-client" @@ -46,10 +46,10 @@ const ( clientSecretDataKey = "clientSecret" // Constants related to the OIDC provider discovery cache. These do not affect the cache of JWKS. - validatorCacheTTL = 15 * time.Minute + oidcValidatorCacheTTL = 15 * time.Minute // Constants related to conditions. - typeClientCredsValid = "ClientCredentialsValid" + typeClientCredentialsValid = "ClientCredentialsValid" typeOIDCDiscoverySucceeded = "OIDCDiscoverySucceeded" reasonNotFound = "SecretNotFound" reasonWrongType = "SecretWrongType" @@ -60,12 +60,12 @@ const ( reasonInvalidResponse = "InvalidResponse" // Errors that are generated by our reconcile process. - errFailureStatus = constable.Error("OIDCIdentityProvider has a failing condition") - errNoCertificates = constable.Error("no certificates found") + errOIDCFailureStatus = constable.Error("OIDCIdentityProvider has a failing condition") + errNoCertificates = constable.Error("no certificates found") ) -// IDPCache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. -type IDPCache interface { +// UpstreamOIDCIdentityProviderICache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. +type UpstreamOIDCIdentityProviderICache interface { SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI) } @@ -86,7 +86,7 @@ func (c *lruValidatorCache) getProvider(spec *v1alpha1.OIDCIdentityProviderSpec) } func (c *lruValidatorCache) putProvider(spec *v1alpha1.OIDCIdentityProviderSpec, provider *oidc.Provider, client *http.Client) { - c.cache.Set(c.cacheKey(spec), &lruValidatorCacheEntry{provider: provider, client: client}, validatorCacheTTL) + c.cache.Set(c.cacheKey(spec), &lruValidatorCacheEntry{provider: provider, client: client}, oidcValidatorCacheTTL) } func (c *lruValidatorCache) cacheKey(spec *v1alpha1.OIDCIdentityProviderSpec) interface{} { @@ -98,8 +98,8 @@ func (c *lruValidatorCache) cacheKey(spec *v1alpha1.OIDCIdentityProviderSpec) in return key } -type controller struct { - cache IDPCache +type oidcWatcherController struct { + cache UpstreamOIDCIdentityProviderICache log logr.Logger client pinnipedclientset.Interface oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer @@ -110,25 +110,25 @@ type controller struct { } } -// New instantiates a new controllerlib.Controller which will populate the provided IDPCache. -func New( - idpCache IDPCache, +// NewOIDCUpstreamWatcherController instantiates a new controllerlib.Controller which will populate the provided UpstreamOIDCIdentityProviderICache. +func NewOIDCUpstreamWatcherController( + idpCache UpstreamOIDCIdentityProviderICache, client pinnipedclientset.Interface, oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer, secretInformer corev1informers.SecretInformer, log logr.Logger, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { - c := controller{ + c := oidcWatcherController{ cache: idpCache, - log: log.WithName(controllerName), + log: log.WithName(oidcControllerName), client: client, oidcIdentityProviderInformer: oidcIdentityProviderInformer, secretInformer: secretInformer, validatorCache: &lruValidatorCache{cache: cache.NewExpiring()}, } return controllerlib.New( - controllerlib.Config{Name: controllerName, Syncer: &c}, + controllerlib.Config{Name: oidcControllerName, Syncer: &c}, withInformer( oidcIdentityProviderInformer, pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), @@ -143,7 +143,7 @@ func New( } // Sync implements controllerlib.Syncer. -func (c *controller) Sync(ctx controllerlib.Context) error { +func (c *oidcWatcherController) Sync(ctx controllerlib.Context) error { actualUpstreams, err := c.oidcIdentityProviderInformer.Lister().List(labels.Everything()) if err != nil { return fmt.Errorf("failed to list OIDCIdentityProviders: %w", err) @@ -168,11 +168,11 @@ func (c *controller) Sync(ctx controllerlib.Context) error { // validateUpstream validates the provided v1alpha1.OIDCIdentityProvider and returns the validated configuration as a // provider.UpstreamOIDCIdentityProvider. As a side effect, it also updates the status of the v1alpha1.OIDCIdentityProvider. -func (c *controller) validateUpstream(ctx controllerlib.Context, upstream *v1alpha1.OIDCIdentityProvider) *upstreamoidc.ProviderConfig { +func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upstream *v1alpha1.OIDCIdentityProvider) *upstreamoidc.ProviderConfig { result := upstreamoidc.ProviderConfig{ Name: upstream.Name, Config: &oauth2.Config{ - Scopes: computeScopes(upstream.Spec.AuthorizationConfig.AdditionalScopes), + Scopes: c.computeScopes(upstream.Spec.AuthorizationConfig.AdditionalScopes), }, UsernameClaim: upstream.Spec.Claims.Username, GroupsClaim: upstream.Spec.Claims.Groups, @@ -192,7 +192,7 @@ func (c *controller) validateUpstream(ctx controllerlib.Context, upstream *v1alp "type", condition.Type, "reason", condition.Reason, "message", condition.Message, - ).Error(errFailureStatus, "found failing condition") + ).Error(errOIDCFailureStatus, "found failing condition") } } if valid { @@ -202,14 +202,14 @@ func (c *controller) validateUpstream(ctx controllerlib.Context, upstream *v1alp } // validateSecret validates the .spec.client.secretName field and returns the appropriate ClientCredentialsValid condition. -func (c *controller) validateSecret(upstream *v1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) *v1alpha1.Condition { +func (c *oidcWatcherController) validateSecret(upstream *v1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) *v1alpha1.Condition { secretName := upstream.Spec.Client.SecretName // Fetch the Secret from informer cache. secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) if err != nil { return &v1alpha1.Condition{ - Type: typeClientCredsValid, + Type: typeClientCredentialsValid, Status: v1alpha1.ConditionFalse, Reason: reasonNotFound, Message: err.Error(), @@ -219,7 +219,7 @@ func (c *controller) validateSecret(upstream *v1alpha1.OIDCIdentityProvider, res // Validate the secret .type field. if secret.Type != oidcClientSecretType { return &v1alpha1.Condition{ - Type: typeClientCredsValid, + Type: typeClientCredentialsValid, Status: v1alpha1.ConditionFalse, Reason: reasonWrongType, Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", secretName, secret.Type, oidcClientSecretType), @@ -231,7 +231,7 @@ func (c *controller) validateSecret(upstream *v1alpha1.OIDCIdentityProvider, res clientSecret := secret.Data[clientSecretDataKey] if len(clientID) == 0 || len(clientSecret) == 0 { return &v1alpha1.Condition{ - Type: typeClientCredsValid, + Type: typeClientCredentialsValid, Status: v1alpha1.ConditionFalse, Reason: reasonMissingKeys, Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", secretName, []string{clientIDDataKey, clientSecretDataKey}), @@ -242,7 +242,7 @@ func (c *controller) validateSecret(upstream *v1alpha1.OIDCIdentityProvider, res result.Config.ClientID = string(clientID) result.Config.ClientSecret = string(clientSecret) return &v1alpha1.Condition{ - Type: typeClientCredsValid, + Type: typeClientCredentialsValid, Status: v1alpha1.ConditionTrue, Reason: reasonSuccess, Message: "loaded client credentials", @@ -250,13 +250,13 @@ func (c *controller) validateSecret(upstream *v1alpha1.OIDCIdentityProvider, res } // validateIssuer validates the .spec.issuer field, performs OIDC discovery, and returns the appropriate OIDCDiscoverySucceeded condition. -func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) *v1alpha1.Condition { +func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1alpha1.OIDCIdentityProvider, result *upstreamoidc.ProviderConfig) *v1alpha1.Condition { // Get the provider and HTTP Client from cache if possible. discoveredProvider, httpClient := c.validatorCache.getProvider(&upstream.Spec) // If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache. if discoveredProvider == nil { - tlsConfig, err := getTLSConfig(upstream) + tlsConfig, err := c.getTLSConfig(upstream) if err != nil { return &v1alpha1.Condition{ Type: typeOIDCDiscoverySucceeded, @@ -312,7 +312,7 @@ func (c *controller) validateIssuer(ctx context.Context, upstream *v1alpha1.OIDC } } -func getTLSConfig(upstream *v1alpha1.OIDCIdentityProvider) (*tls.Config, error) { +func (*oidcWatcherController) getTLSConfig(upstream *v1alpha1.OIDCIdentityProvider) (*tls.Config, error) { result := tls.Config{ MinVersion: tls.VersionTLS12, } @@ -334,7 +334,7 @@ func getTLSConfig(upstream *v1alpha1.OIDCIdentityProvider) (*tls.Config, error) return &result, nil } -func (c *controller) updateStatus(ctx context.Context, upstream *v1alpha1.OIDCIdentityProvider, conditions []*v1alpha1.Condition) { +func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.OIDCIdentityProvider, conditions []*v1alpha1.Condition) { log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() @@ -344,7 +344,7 @@ func (c *controller) updateStatus(ctx context.Context, upstream *v1alpha1.OIDCId cond := conditions[i].DeepCopy() cond.LastTransitionTime = metav1.Now() cond.ObservedGeneration = upstream.Generation - if mergeCondition(&updated.Status.Conditions, cond) { + if c.mergeCondition(&updated.Status.Conditions, cond) { log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) } if cond.Status == v1alpha1.ConditionFalse { @@ -371,7 +371,7 @@ func (c *controller) updateStatus(ctx context.Context, upstream *v1alpha1.OIDCId // mergeCondition merges a new v1alpha1.Condition into a slice of existing conditions. It returns true // if the condition has meaningfully changed. -func mergeCondition(existing *[]v1alpha1.Condition, new *v1alpha1.Condition) bool { +func (*oidcWatcherController) mergeCondition(existing *[]v1alpha1.Condition, new *v1alpha1.Condition) bool { // Find any existing condition with a matching type. var old *v1alpha1.Condition for i := range *existing { @@ -403,7 +403,7 @@ func mergeCondition(existing *[]v1alpha1.Condition, new *v1alpha1.Condition) boo return false } -func computeScopes(additionalScopes []string) []string { +func (*oidcWatcherController) computeScopes(additionalScopes []string) []string { // First compute the unique set of scopes, including "openid" (de-duplicate). set := make(map[string]bool, len(additionalScopes)+1) set["openid"] = true diff --git a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go similarity index 78% rename from internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go rename to internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go index bb0cf454..9b8486dd 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/upstreamwatcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go @@ -82,7 +82,7 @@ func TestControllerFilterSecret(t *testing.T) { secretInformer := kubeInformers.Core().V1().Secrets() withInformer := testutil.NewObservableWithInformerOption() - New( + NewOIDCUpstreamWatcherController( cache, nil, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), @@ -150,9 +150,9 @@ func TestController(t *testing.T) { inputSecrets: []runtime.Object{}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="secret \"test-client-secret\" not found" "reason"="SecretNotFound" "status"="False" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, - `upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="secret \"test-client-secret\" not found" "name"="test-name" "namespace"="test-namespace" "reason"="SecretNotFound" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="secret \"test-client-secret\" not found" "reason"="SecretNotFound" "status"="False" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="secret \"test-client-secret\" not found" "name"="test-name" "namespace"="test-namespace" "reason"="SecretNotFound" "type"="ClientCredentialsValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -196,9 +196,9 @@ func TestController(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "reason"="SecretWrongType" "status"="False" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, - `upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "name"="test-name" "namespace"="test-namespace" "reason"="SecretWrongType" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "reason"="SecretWrongType" "status"="False" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" has wrong type \"some-other-type\" (should be \"secrets.pinniped.dev/oidc-client\")" "name"="test-name" "namespace"="test-namespace" "reason"="SecretWrongType" "type"="ClientCredentialsValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -241,9 +241,9 @@ func TestController(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "reason"="SecretMissingKeys" "status"="False" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, - `upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "name"="test-name" "namespace"="test-namespace" "reason"="SecretMissingKeys" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "reason"="SecretMissingKeys" "status"="False" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="referenced Secret \"test-client-secret\" is missing required keys [\"clientID\" \"clientSecret\"]" "name"="test-name" "namespace"="test-namespace" "reason"="SecretMissingKeys" "type"="ClientCredentialsValid"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -289,9 +289,9 @@ func TestController(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, - `upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: illegal base64 data at input byte 7" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -337,9 +337,9 @@ func TestController(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, - `upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="spec.certificateAuthorityData is invalid: no certificates found" "reason"="InvalidTLSConfig" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="spec.certificateAuthorityData is invalid: no certificates found" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidTLSConfig" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -382,9 +382,9 @@ func TestController(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, - `upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to perform OIDC discovery against \"invalid-url\"" "reason"="Unreachable" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to perform OIDC discovery against \"invalid-url\"" "name"="test-name" "namespace"="test-namespace" "reason"="Unreachable" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -428,9 +428,9 @@ func TestController(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, - `upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="failed to parse authorization endpoint URL: parse \"%\": invalid URL escape \"%\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -474,9 +474,9 @@ func TestController(t *testing.T) { }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, - `upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "reason"="InvalidResponse" "status"="False" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "msg"="found failing condition" "error"="OIDCIdentityProvider has a failing condition" "message"="authorization endpoint URL scheme must be \"https\", not \"http\"" "name"="test-name" "namespace"="test-namespace" "reason"="InvalidResponse" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{}, wantResultingUpstreams: []v1alpha1.OIDCIdentityProvider{{ @@ -527,8 +527,8 @@ func TestController(t *testing.T) { Data: testValidSecretData, }}, wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -576,8 +576,8 @@ func TestController(t *testing.T) { Data: testValidSecretData, }}, wantLogs: []string{ - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, - `upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="loaded client credentials" "reason"="Success" "status"="True" "type"="ClientCredentialsValid"`, + `oidc-upstream-observer "level"=0 "msg"="updated condition" "name"="test-name" "namespace"="test-namespace" "message"="discovered issuer configuration" "reason"="Success" "status"="True" "type"="OIDCDiscoverySucceeded"`, }, wantResultingCache: []provider.UpstreamOIDCIdentityProviderI{ &oidctestutil.TestUpstreamOIDCIdentityProvider{ @@ -615,7 +615,7 @@ func TestController(t *testing.T) { &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) - controller := New( + controller := NewOIDCUpstreamWatcherController( cache, fakePinnipedClient, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), From 05daa9eff5a33cc2d3589f6aa83615f15b109c06 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 9 Apr 2021 18:49:43 -0700 Subject: [PATCH 09/59] More LDAP WIP: started controller and LDAP server connection code Both are unfinished works in progress. --- cmd/pinniped-supervisor/main.go | 11 + .../authenticators.go} | 4 +- .../upstreamwatcher/ldap_upstream_watcher.go | 97 +++++++ .../ldap_upstream_watcher_test.go | 253 ++++++++++++++++++ .../oidc_upstream_watcher_test.go | 4 +- .../provider/dynamic_upstream_idp_provider.go | 4 +- internal/upstreamldap/upstreamldap.go | 151 +++++++++-- internal/upstreamldap/upstreamldap_test.go | 178 +++++++++++- 8 files changed, 668 insertions(+), 34 deletions(-) rename internal/{ldap/ldap.go => authenticators/authenticators.go} (93%) create mode 100644 internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go create mode 100644 internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 65ec1c13..9acf404f 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -241,6 +241,17 @@ func startControllers( klogr.New(), controllerlib.WithInformer, ), + singletonWorker). + WithController( + upstreamwatcher.NewLDAPUpstreamWatcherController( + dynamicUpstreamIDPProvider, + // nil means to use a real production dialer when creating objects to add to the dynamicUpstreamIDPProvider cache. + nil, + pinnipedClient, + pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), + secretInformer, + controllerlib.WithInformer, + ), singletonWorker) kubeInformers.Start(ctx.Done()) diff --git a/internal/ldap/ldap.go b/internal/authenticators/authenticators.go similarity index 93% rename from internal/ldap/ldap.go rename to internal/authenticators/authenticators.go index a5899be2..bd24ff0b 100644 --- a/internal/ldap/ldap.go +++ b/internal/authenticators/authenticators.go @@ -1,8 +1,8 @@ // Copyright 2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package ldap contains common LDAP functionality needed by Pinniped. -package ldap +// Package authenticators contains authenticator interfaces. +package authenticators import ( "context" diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go new file mode 100644 index 00000000..56932005 --- /dev/null +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -0,0 +1,97 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package upstreamwatcher + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + corev1informers "k8s.io/client-go/informers/core/v1" + + "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" + idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1" + pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/upstreamldap" +) + +const ( + ldapControllerName = "ldap-upstream-observer" + ldapBindAccountSecretType = corev1.SecretTypeBasicAuth +) + +// UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. +type UpstreamLDAPIdentityProviderICache interface { + SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) +} + +type ldapWatcherController struct { + cache UpstreamLDAPIdentityProviderICache + ldapDialFunc upstreamldap.LDAPDialerFunc + client pinnipedclientset.Interface + ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer + secretInformer corev1informers.SecretInformer +} + +// NewLDAPUpstreamWatcherController instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. +func NewLDAPUpstreamWatcherController( + idpCache UpstreamLDAPIdentityProviderICache, + ldapDialFunc upstreamldap.LDAPDialerFunc, + client pinnipedclientset.Interface, + ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, + secretInformer corev1informers.SecretInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, +) controllerlib.Controller { + c := ldapWatcherController{ + cache: idpCache, + ldapDialFunc: ldapDialFunc, + client: client, + ldapIdentityProviderInformer: ldapIdentityProviderInformer, + secretInformer: secretInformer, + } + return controllerlib.New( + controllerlib.Config{Name: ldapControllerName, Syncer: &c}, + withInformer( + ldapIdentityProviderInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + secretInformer, + pinnipedcontroller.MatchAnySecretOfTypeFilter(ldapBindAccountSecretType, pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + ) +} + +// Sync implements controllerlib.Syncer. +func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { + actualUpstreams, err := c.ldapIdentityProviderInformer.Lister().List(labels.Everything()) + if err != nil { + return fmt.Errorf("failed to list LDAPIdentityProviders: %w", err) + } + + requeue := false + validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams)) + for _, upstream := range actualUpstreams { + valid := c.validateUpstream(upstream) + if valid == nil { + requeue = true + } else { + validatedUpstreams = append(validatedUpstreams, valid) + } + } + c.cache.SetLDAPIdentityProviders(validatedUpstreams) + if requeue { + return controllerlib.ErrSyntheticRequeue + } + return nil +} + +func (c *ldapWatcherController) validateUpstream(upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI { + return &upstreamldap.Provider{Name: upstream.Name, Dial: c.ldapDialFunc} +} diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go new file mode 100644 index 00000000..e1e688fb --- /dev/null +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -0,0 +1,253 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package upstreamwatcher + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes/fake" + + "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" + pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/upstreamldap" +) + +func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + secret metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "a secret of the right type", + secret: &corev1.Secret{ + Type: corev1.SecretTypeBasicAuth, + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "a secret of the wrong type", + secret: &corev1.Secret{ + Type: "this-is-the-wrong-type", + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + }, + { + name: "resource of a data type which is not watched by this controller", + secret: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + fakePinnipedClient := pinnipedfake.NewSimpleClientset() + pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) + ldapIDPInformer := pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders() + fakeKubeClient := fake.NewSimpleClientset() + kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) + secretInformer := kubeInformers.Core().V1().Secrets() + withInformer := testutil.NewObservableWithInformerOption() + + NewLDAPUpstreamWatcherController(nil, nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) + + unrelated := corev1.Secret{} + filter := withInformer.GetFilterForInformer(secretInformer) + require.Equal(t, test.wantAdd, filter.Add(test.secret)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, test.secret)) + require.Equal(t, test.wantUpdate, filter.Update(test.secret, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(test.secret)) + }) + } +} + +func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + idp metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "any LDAPIdentityProvider", + idp: &v1alpha1.LDAPIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + fakePinnipedClient := pinnipedfake.NewSimpleClientset() + pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) + ldapIDPInformer := pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders() + fakeKubeClient := fake.NewSimpleClientset() + kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) + secretInformer := kubeInformers.Core().V1().Secrets() + withInformer := testutil.NewObservableWithInformerOption() + + NewLDAPUpstreamWatcherController(nil, nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) + + unrelated := corev1.Secret{} + filter := withInformer.GetFilterForInformer(ldapIDPInformer) + require.Equal(t, test.wantAdd, filter.Add(test.idp)) + require.Equal(t, test.wantUpdate, filter.Update(&unrelated, test.idp)) + require.Equal(t, test.wantUpdate, filter.Update(test.idp, &unrelated)) + require.Equal(t, test.wantDelete, filter.Delete(test.idp)) + }) + } +} + +func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { + t.Parallel() + + var ( + testNamespace = "test-namespace" + testName = "test-name" + testSecretName = "test-client-secret" + testBindUsername = "test-bind-username" + testBindPassword = "test-bind-password" + testValidSecretData = map[string][]byte{"username": []byte(testBindUsername), "password": []byte(testBindPassword)} + ) + tests := []struct { + name string + inputUpstreams []runtime.Object + inputSecrets []runtime.Object + wantErr string + wantResultingCache []provider.UpstreamLDAPIdentityProviderI + wantResultingUpstreams []v1alpha1.LDAPIdentityProvider + }{ + { + name: "no LDAPIdentityProvider upstreams clears the cache", + }, + { + name: "one valid upstream updates the cache to include only that upstream", + inputUpstreams: []runtime.Object{&v1alpha1.LDAPIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, + Spec: v1alpha1.LDAPIdentityProviderSpec{ + Host: "TODO", // TODO + TLS: &v1alpha1.LDAPIdentityProviderTLSSpec{CertificateAuthorityData: "TODO"}, // TODO + Bind: v1alpha1.LDAPIdentityProviderBindSpec{SecretName: testSecretName}, + UserSearch: v1alpha1.LDAPIdentityProviderUserSearchSpec{ + Base: "TODO", // TODO + Filter: "TODO", // TODO + Attributes: v1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ + Username: "TODO", // TODO + UniqueID: "TODO", // TODO + }, + }, + }, + }}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + wantResultingCache: []provider.UpstreamLDAPIdentityProviderI{ + &upstreamldap.Provider{ + Name: testName, + // TODO test more stuff + }, + }, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + // TODO Conditions + }, + }}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fakePinnipedClient := pinnipedfake.NewSimpleClientset(tt.inputUpstreams...) + pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) + fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) + kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) + cache := provider.NewDynamicUpstreamIDPProvider() + cache.SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + &upstreamldap.Provider{Name: "initial-entry"}, + }) + + controller := NewLDAPUpstreamWatcherController( + cache, + func(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) { + // TODO return a fake implementation of upstreamldap.Conn, or return an error for testing errors + return nil, nil + }, + fakePinnipedClient, + pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), + kubeInformers.Core().V1().Secrets(), + controllerlib.WithInformer, + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + pinnipedInformers.Start(ctx.Done()) + kubeInformers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, controller) + + syncCtx := controllerlib.Context{Context: ctx, Key: controllerlib.Key{}} + + if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + + actualIDPList := cache.GetLDAPIdentityProviders() + require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) + for i := range actualIDPList { + actualIDP := actualIDPList[i].(*upstreamldap.Provider) + require.Equal(t, tt.wantResultingCache[i].GetName(), actualIDP.GetName()) + // TODO more assertions + } + + actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + + // TODO maybe use something like the normalizeUpstreams() helper to make assertions about what was updated + _ = actualUpstreams + // require.ElementsMatch(t, tt.wantResultingUpstreams, actualUpstreams.Items) + + // Running the sync() a second time should be idempotent, and should return the same error. + if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go index 9b8486dd..35ffcd99 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go @@ -31,7 +31,7 @@ import ( "go.pinniped.dev/internal/upstreamoidc" ) -func TestControllerFilterSecret(t *testing.T) { +func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { t.Parallel() tests := []struct { @@ -101,7 +101,7 @@ func TestControllerFilterSecret(t *testing.T) { } } -func TestController(t *testing.T) { +func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { t.Parallel() now := metav1.NewTime(time.Now().UTC()) earlier := metav1.NewTime(now.Add(-1 * time.Hour).UTC()) diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index 59175fac..50965abc 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -10,7 +10,7 @@ import ( "golang.org/x/oauth2" - "go.pinniped.dev/internal/ldap" + "go.pinniped.dev/internal/authenticators" "go.pinniped.dev/pkg/oidcclient/nonce" "go.pinniped.dev/pkg/oidcclient/oidctypes" "go.pinniped.dev/pkg/oidcclient/pkce" @@ -59,7 +59,7 @@ type UpstreamLDAPIdentityProviderI interface { GetURL() string // A method for performing user authentication against the upstream LDAP provider. - ldap.UserAuthenticator + authenticators.UserAuthenticator } type DynamicUpstreamIDPProvider interface { diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index bc8a7eee..b8ed92e2 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -6,65 +6,168 @@ package upstreamldap import ( "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "strings" ldap "github.com/go-ldap/ldap/v3" "k8s.io/apiserver/pkg/authentication/authenticator" ) +const ( + ldapsScheme = "ldaps" +) + // Conn abstracts the upstream LDAP communication protocol (mostly for testing). type Conn interface { - // Bind abstracts ldap.Conn.Bind(). Bind(username, password string) error - // Search abstracts ldap.Conn.Search(). + Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) - // Close abstracts ldap.Conn.Close(). + Close() } +// Our Conn type is subset of the ldap.Client interface, which is implemented by ldap.Conn. +var _ Conn = &ldap.Conn{} + +// LDAPDialerFunc is a factory of Conn, and the resulting Conn can then be used to interact with an upstream LDAP IDP. +type LDAPDialerFunc func(ctx context.Context, hostAndPort string) (Conn, error) + +// Provider includes all of the settings for connection and searching for users and groups in +// the upstream LDAP IDP. It also provides methods for testing the connection and performing logins. +type Provider struct { + // Name is the unique name of this upstream LDAP IDP. + Name string + + // Host is the hostname or "hostname:port" of the LDAP server. When the port is not specified, + // the default LDAP port will be used. + Host string + + // PEM-encoded CA cert bundle to trust when connecting to the LDAP server. + CABundle []byte + + // BindUsername is the username to use when performing a bind with the upstream LDAP IDP. + BindUsername string + + // BindPassword is the password to use when performing a bind with the upstream LDAP IDP. + BindPassword string + + // UserSearch contains information about how to search for users in the upstream LDAP IDP. + UserSearch *UserSearch + + // Dial exists to enable testing. When nil, will use a default appropriate for production use. + Dial LDAPDialerFunc +} + // UserSearch contains information about how to search for users in the upstream LDAP IDP. type UserSearch struct { // Base is the base DN to use for the user search in the upstream LDAP IDP. Base string + // Filter is the filter to use for the user search in the upstream LDAP IDP. Filter string + // UsernameAttribute is the attribute in the LDAP entry from which the username should be // retrieved. UsernameAttribute string + // UIDAttribute is the attribute in the LDAP entry from which the user's unique ID should be // retrieved. UIDAttribute string } -// Provider contains can interact with an upstream LDAP IDP. -type Provider struct { - // Name is the unique name of this upstream LDAP IDP. - Name string - // URL is the URL of this upstream LDAP IDP. - URL string - - // Dial is a func that, given a URL, will return an LDAPConn to use for communicating with an - // upstream LDAP IDP. - Dial func(ctx context.Context, url string) (Conn, error) - - // BindUsername is the username to use when performing a bind with the upstream LDAP IDP. - BindUsername string - // BindPassword is the password to use when performing a bind with the upstream LDAP IDP. - BindPassword string - - // UserSearch contains information about how to search for users in the upstream LDAP IDP. - UserSearch *UserSearch +func (p *Provider) dial(ctx context.Context) (Conn, error) { + hostAndPort, err := hostAndPortWithDefaultPort(p.Host, ldap.DefaultLdapsPort) + if err != nil { + return nil, ldap.NewError(ldap.ErrorNetwork, err) + } + if p.Dial != nil { + return p.Dial(ctx, hostAndPort) + } + return p.dialTLS(ctx, hostAndPort) } +// dialTLS is the default implementation of the Dial func, used when Dial is nil. +// Unfortunately, the go-ldap library does not seem to support dialing with a context.Context, +// so we implement it ourselves, heavily inspired by ldap.DialURL. +func (p *Provider) dialTLS(ctx context.Context, hostAndPort string) (Conn, error) { + rootCAs := x509.NewCertPool() + if p.CABundle != nil { + if !rootCAs.AppendCertsFromPEM(p.CABundle) { + return nil, ldap.NewError(ldap.ErrorNetwork, fmt.Errorf("could not parse CA bundle")) + } + } + + dialer := &tls.Dialer{Config: &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: rootCAs, + }} + + c, err := dialer.DialContext(ctx, "tcp", hostAndPort) + if err != nil { + return nil, ldap.NewError(ldap.ErrorNetwork, err) + } + + conn := ldap.NewConn(c, true) + conn.Start() + return conn, nil +} + +// Adds the default port if hostAndPort did not already include a port. +func hostAndPortWithDefaultPort(hostAndPort string, defaultPort string) (string, error) { + host, port, err := net.SplitHostPort(hostAndPort) + if err != nil { + if strings.HasSuffix(err.Error(), ": missing port in address") { // sad to need to do this string compare + host = hostAndPort + port = defaultPort + } else { + return "", err // hostAndPort argument was not parsable + } + } + switch { + case port != "" && strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]"): + // don't add extra square brackets to an IPv6 address that already has them + return host + ":" + port, nil + case port != "": + return net.JoinHostPort(host, port), nil + default: + return host, nil + } +} + +// A name for this upstream provider. func (p *Provider) GetName() string { return p.Name } +// Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234". +// This URL is not used for connecting to the provider, but rather is used for creating a globally unique user +// identifier by being combined with the user's UID, since user UIDs are only unique within one provider. func (p *Provider) GetURL() string { - return p.URL + return fmt.Sprintf("%s://%s", ldapsScheme, p.Host) } +// TestConnection provides a method for testing the connection and bind settings by dialing and binding. +func (p *Provider) TestConnection(ctx context.Context) error { + _, _ = p.dial(ctx) + // TODO bind using the bind credentials + // TODO close + // TODO return any dial or bind errors + return nil +} + +// Authenticate a 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) { - // TODO: test context timeout? - // TODO: test dial context timeout? + _, _ = p.dial(ctx) + // TODO bind + // TODO user search + // TODO user bind + // TODO map username and uid attributes + // TODO group search + // TODO map group attributes + // TODO close + // TODO return any errors that were encountered along the way return nil, false, nil } diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 7f739144..f031838b 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -5,15 +5,21 @@ package upstreamldap import ( "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" "testing" - ldap "github.com/go-ldap/ldap/v3" + "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/mocks/mockldapconn" + "go.pinniped.dev/internal/testutil" ) var ( @@ -39,7 +45,7 @@ func TestAuthenticateUser(t *testing.T) { { name: "happy path", provider: &Provider{ - URL: "ldaps://some-ldap-url:1234", + Host: "ldap.example.com:8443", BindUsername: upstreamUsername, BindPassword: upstreamPassword, UserSearch: &UserSearch{ @@ -87,12 +93,15 @@ func TestAuthenticateUser(t *testing.T) { }, nil).Times(1) conn.EXPECT().Close().Times(1) - test.provider.Dial = func(ctx context.Context, url string) (Conn, error) { - require.Equal(t, test.provider.URL, url) + dialWasAttempted := false + test.provider.Dial = func(ctx context.Context, hostAndPort string) (Conn, error) { + dialWasAttempted = true + require.Equal(t, test.provider.Host, hostAndPort) return conn, nil } authResponse, authenticated, err := test.provider.AuthenticateUser(context.Background(), upstreamUsername, upstreamPassword) + require.True(t, dialWasAttempted, "AuthenticateUser was supposed to try to dial, but didn't") if test.wantError != "" { require.EqualError(t, err, test.wantError) return @@ -102,3 +111,164 @@ func TestAuthenticateUser(t *testing.T) { }) } } + +func TestGetURL(t *testing.T) { + require.Equal(t, "ldaps://ldap.example.com:1234", (&Provider{Host: "ldap.example.com:1234"}).GetURL()) + require.Equal(t, "ldaps://ldap.example.com", (&Provider{Host: "ldap.example.com"}).GetURL()) +} + +// Testing of host parsing, TLS negotiation, and CA bundle, etc. for the production code's dialer. +func TestRealTLSDialing(t *testing.T) { + testServerCABundle, testServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {}) + parsedURL, err := url.Parse(testServerURL) + require.NoError(t, err) + testServerHostAndPort := parsedURL.Host + + unusedPortGrabbingListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + recentlyClaimedHostAndPort := unusedPortGrabbingListener.Addr().String() + require.NoError(t, unusedPortGrabbingListener.Close()) + + alreadyCancelledContext, cancelFunc := context.WithCancel(context.Background()) + cancelFunc() // cancel it immediately + + tests := []struct { + name string + host string + caBundle []byte + context context.Context + wantError string + }{ + { + name: "happy path", + host: testServerHostAndPort, + caBundle: []byte(testServerCABundle), + context: context.Background(), + }, + { + name: "invalid CA bundle", + host: testServerHostAndPort, + caBundle: []byte("not a ca bundle"), + context: context.Background(), + wantError: `LDAP Result Code 200 "Network Error": could not parse CA bundle`, + }, + { + name: "missing CA bundle when it is required because the host is not using a trusted CA", + host: testServerHostAndPort, + caBundle: nil, + context: context.Background(), + wantError: `LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, + }, + { + name: "cannot connect to host", + // This is assuming that this port was not reclaimed by another app since the test setup ran. Seems safe enough. + host: recentlyClaimedHostAndPort, + caBundle: []byte(testServerCABundle), + context: context.Background(), + wantError: fmt.Sprintf(`LDAP Result Code 200 "Network Error": dial tcp %s: connect: connection refused`, recentlyClaimedHostAndPort), + }, + { + name: "pays attention to the passed context", + host: testServerHostAndPort, + caBundle: []byte(testServerCABundle), + context: alreadyCancelledContext, + wantError: fmt.Sprintf(`LDAP Result Code 200 "Network Error": dial tcp %s: operation was canceled`, testServerHostAndPort), + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + provider := &Provider{ + Host: test.host, + CABundle: test.caBundle, + Dial: nil, // this test is for the default (production) dialer + } + conn, err := provider.dial(test.context) + if conn != nil { + defer conn.Close() + } + if test.wantError != "" { + require.Nil(t, conn) + require.EqualError(t, err, test.wantError) + } else { + require.NoError(t, err) + require.NotNil(t, conn) + + // Should be an instance of the real production LDAP client type. + // Can't test its methods here because we are not dialed to a real LDAP server. + require.IsType(t, &ldap.Conn{}, conn) + + // Indirectly checking that the Dial method constructed the ldap.Conn with isTLS set to true, + // since this is always the correct behavior unless/until we want to support StartTLS. + err := conn.(*ldap.Conn).StartTLS(&tls.Config{}) + require.EqualError(t, err, `LDAP Result Code 200 "Network Error": ldap: already encrypted`) + } + }) + } +} + +// Test various cases of host and port parsing. +func TestHostAndPortWithDefaultPort(t *testing.T) { + tests := []struct { + name string + hostAndPort string + defaultPort string + wantError string + wantHostAndPort string + }{ + { + name: "host already has port", + hostAndPort: "host.example.com:99", + defaultPort: "42", + wantHostAndPort: "host.example.com:99", + }, + { + name: "host does not have port", + hostAndPort: "host.example.com", + defaultPort: "42", + wantHostAndPort: "host.example.com:42", + }, + { + name: "host does not have port and default port is empty", + hostAndPort: "host.example.com", + defaultPort: "", + wantHostAndPort: "host.example.com", + }, + { + name: "IPv6 host already has port", + hostAndPort: "[::1%lo0]:80", + defaultPort: "42", + wantHostAndPort: "[::1%lo0]:80", + }, + { + name: "IPv6 host does not have port", + hostAndPort: "[::1%lo0]", + defaultPort: "42", + wantHostAndPort: "[::1%lo0]:42", + }, + { + name: "IPv6 host does not have port and default port is empty", + hostAndPort: "[::1%lo0]", + defaultPort: "", + wantHostAndPort: "[::1%lo0]", + }, + { + name: "host is not valid", + hostAndPort: "host.example.com:port1:port2", + defaultPort: "42", + wantError: "address host.example.com:port1:port2: too many colons in address", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + hostAndPort, err := hostAndPortWithDefaultPort(test.hostAndPort, test.defaultPort) + if test.wantError != "" { + require.EqualError(t, err, test.wantError) + } else { + require.NoError(t, err) + } + require.Equal(t, test.wantHostAndPort, hostAndPort) + }) + } +} From 05571abb7436ecd4e0f9a0ab43c8fd505f523692 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 12 Apr 2021 11:23:08 -0700 Subject: [PATCH 10/59] Add a little more logic to ldap_upstream_watcher.go --- .../upstreamwatcher/ldap_upstream_watcher.go | 47 ++++++++++- .../ldap_upstream_watcher_test.go | 79 +++++++++++++------ internal/upstreamldap/upstreamldap.go | 23 ++++-- internal/upstreamldap/upstreamldap_test.go | 8 +- 4 files changed, 119 insertions(+), 38 deletions(-) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index 56932005..207d7a79 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -31,7 +31,7 @@ type UpstreamLDAPIdentityProviderICache interface { type ldapWatcherController struct { cache UpstreamLDAPIdentityProviderICache - ldapDialFunc upstreamldap.LDAPDialerFunc + ldapDialer upstreamldap.LDAPDialer client pinnipedclientset.Interface ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer secretInformer corev1informers.SecretInformer @@ -40,7 +40,7 @@ type ldapWatcherController struct { // NewLDAPUpstreamWatcherController instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. func NewLDAPUpstreamWatcherController( idpCache UpstreamLDAPIdentityProviderICache, - ldapDialFunc upstreamldap.LDAPDialerFunc, + ldapDialer upstreamldap.LDAPDialer, client pinnipedclientset.Interface, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, secretInformer corev1informers.SecretInformer, @@ -48,7 +48,7 @@ func NewLDAPUpstreamWatcherController( ) controllerlib.Controller { c := ldapWatcherController{ cache: idpCache, - ldapDialFunc: ldapDialFunc, + ldapDialer: ldapDialer, client: client, ldapIdentityProviderInformer: ldapIdentityProviderInformer, secretInformer: secretInformer, @@ -93,5 +93,44 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { } func (c *ldapWatcherController) validateUpstream(upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI { - return &upstreamldap.Provider{Name: upstream.Name, Dial: c.ldapDialFunc} + spec := upstream.Spec + result := &upstreamldap.Provider{ + Name: upstream.Name, + Host: spec.Host, + CABundle: []byte(spec.TLS.CertificateAuthorityData), + UserSearch: &upstreamldap.UserSearch{ + Base: spec.UserSearch.Base, + Filter: spec.UserSearch.Filter, + UsernameAttribute: spec.UserSearch.Attributes.Username, + UIDAttribute: spec.UserSearch.Attributes.UniqueID, + }, + Dialer: c.ldapDialer, + } + _ = c.validateSecret(upstream, result) + return result +} + +func (c ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.Provider) *v1alpha1.Condition { + secretName := upstream.Spec.Bind.SecretName + + secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) + if err != nil { + // TODO + return nil + } + + if secret.Type != corev1.SecretTypeBasicAuth { + // TODO + return nil + } + + result.BindUsername = string(secret.Data[corev1.BasicAuthUsernameKey]) + result.BindPassword = string(secret.Data[corev1.BasicAuthPasswordKey]) + if len(result.BindUsername) == 0 || len(result.BindPassword) == 0 { + // TODO + return nil + } + + var cond *v1alpha1.Condition // satisfy linter + return cond } diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index e1e688fb..e7f9f503 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -127,42 +127,69 @@ func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) } } +// Wrap the func into a struct so the test can do deep equal assertions on instances of upstreamldap.Provider. +type comparableDialer struct { + f upstreamldap.LDAPDialerFunc +} + +func (d *comparableDialer) Dial(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) { + return d.f(ctx, hostAndPort) +} + func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { t.Parallel() + const ( + testNamespace = "test-namespace" + testName = "test-name" + testSecretName = "test-client-secret" + testBindUsername = "test-bind-username" + testBindPassword = "test-bind-password" + testHost = "ldap.example.com:123" + testCABundle = "test-ca-bundle" + testUserSearchBase = "test-user-search-base" + testUserSearchFilter = "test-user-search-filter" + testUsernameAttrName = "test-username-attr" + testUIDAttrName = "test-uid-attr" + ) var ( - testNamespace = "test-namespace" - testName = "test-name" - testSecretName = "test-client-secret" - testBindUsername = "test-bind-username" - testBindPassword = "test-bind-password" testValidSecretData = map[string][]byte{"username": []byte(testBindUsername), "password": []byte(testBindPassword)} ) + + successfulDialer := &comparableDialer{ + f: func(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) { + // TODO return a fake implementation of upstreamldap.Conn, or return an error for testing errors + return nil, nil + }, + } + tests := []struct { name string inputUpstreams []runtime.Object inputSecrets []runtime.Object + ldapDialer upstreamldap.LDAPDialer wantErr string - wantResultingCache []provider.UpstreamLDAPIdentityProviderI + wantResultingCache []*upstreamldap.Provider wantResultingUpstreams []v1alpha1.LDAPIdentityProvider }{ { name: "no LDAPIdentityProvider upstreams clears the cache", }, { - name: "one valid upstream updates the cache to include only that upstream", + name: "one valid upstream updates the cache to include only that upstream", + ldapDialer: successfulDialer, inputUpstreams: []runtime.Object{&v1alpha1.LDAPIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, Spec: v1alpha1.LDAPIdentityProviderSpec{ - Host: "TODO", // TODO - TLS: &v1alpha1.LDAPIdentityProviderTLSSpec{CertificateAuthorityData: "TODO"}, // TODO + Host: testHost, + TLS: &v1alpha1.LDAPIdentityProviderTLSSpec{CertificateAuthorityData: testCABundle}, Bind: v1alpha1.LDAPIdentityProviderBindSpec{SecretName: testSecretName}, UserSearch: v1alpha1.LDAPIdentityProviderUserSearchSpec{ - Base: "TODO", // TODO - Filter: "TODO", // TODO + Base: testUserSearchBase, + Filter: testUserSearchFilter, Attributes: v1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ - Username: "TODO", // TODO - UniqueID: "TODO", // TODO + Username: testUsernameAttrName, + UniqueID: testUIDAttrName, }, }, }, @@ -172,10 +199,20 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, - wantResultingCache: []provider.UpstreamLDAPIdentityProviderI{ - &upstreamldap.Provider{ - Name: testName, - // TODO test more stuff + wantResultingCache: []*upstreamldap.Provider{ + { + Name: testName, + Host: testHost, + CABundle: []byte(testCABundle), + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: &upstreamldap.UserSearch{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through }, }, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ @@ -202,10 +239,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { controller := NewLDAPUpstreamWatcherController( cache, - func(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) { - // TODO return a fake implementation of upstreamldap.Conn, or return an error for testing errors - return nil, nil - }, + successfulDialer, fakePinnipedClient, pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), kubeInformers.Core().V1().Secrets(), @@ -231,8 +265,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) for i := range actualIDPList { actualIDP := actualIDPList[i].(*upstreamldap.Provider) - require.Equal(t, tt.wantResultingCache[i].GetName(), actualIDP.GetName()) - // TODO more assertions + require.Equal(t, tt.wantResultingCache[i], actualIDP) } actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index b8ed92e2..8e62f724 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -12,7 +12,7 @@ import ( "net" "strings" - ldap "github.com/go-ldap/ldap/v3" + "github.com/go-ldap/ldap/v3" "k8s.io/apiserver/pkg/authentication/authenticator" ) @@ -32,9 +32,18 @@ type Conn interface { // Our Conn type is subset of the ldap.Client interface, which is implemented by ldap.Conn. var _ Conn = &ldap.Conn{} -// LDAPDialerFunc is a factory of Conn, and the resulting Conn can then be used to interact with an upstream LDAP IDP. +// LDAPDialer is a factory of Conn, and the resulting Conn can then be used to interact with an upstream LDAP IDP. +type LDAPDialer interface { + Dial(ctx context.Context, hostAndPort string) (Conn, error) +} + +// LDAPDialerFunc makes it easy to use a func as an LDAPDialer. type LDAPDialerFunc func(ctx context.Context, hostAndPort string) (Conn, error) +func (f LDAPDialerFunc) Dial(ctx context.Context, hostAndPort string) (Conn, error) { + return f(ctx, hostAndPort) +} + // Provider includes all of the settings for connection and searching for users and groups in // the upstream LDAP IDP. It also provides methods for testing the connection and performing logins. type Provider struct { @@ -57,8 +66,8 @@ type Provider struct { // UserSearch contains information about how to search for users in the upstream LDAP IDP. UserSearch *UserSearch - // Dial exists to enable testing. When nil, will use a default appropriate for production use. - Dial LDAPDialerFunc + // Dialer exists to enable testing. When nil, will use a default appropriate for production use. + Dialer LDAPDialer } // UserSearch contains information about how to search for users in the upstream LDAP IDP. @@ -83,13 +92,13 @@ func (p *Provider) dial(ctx context.Context) (Conn, error) { if err != nil { return nil, ldap.NewError(ldap.ErrorNetwork, err) } - if p.Dial != nil { - return p.Dial(ctx, hostAndPort) + if p.Dialer != nil { + return p.Dialer.Dial(ctx, hostAndPort) } return p.dialTLS(ctx, hostAndPort) } -// dialTLS is the default implementation of the Dial func, used when Dial is nil. +// dialTLS is the default implementation of the Dialer, used when Dialer is nil. // Unfortunately, the go-ldap library does not seem to support dialing with a context.Context, // so we implement it ourselves, heavily inspired by ldap.DialURL. func (p *Provider) dialTLS(ctx context.Context, hostAndPort string) (Conn, error) { diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index f031838b..358582f7 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -94,11 +94,11 @@ func TestAuthenticateUser(t *testing.T) { conn.EXPECT().Close().Times(1) dialWasAttempted := false - test.provider.Dial = func(ctx context.Context, hostAndPort string) (Conn, error) { + test.provider.Dialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) { dialWasAttempted = true require.Equal(t, test.provider.Host, hostAndPort) return conn, nil - } + }) authResponse, authenticated, err := test.provider.AuthenticateUser(context.Background(), upstreamUsername, upstreamPassword) require.True(t, dialWasAttempted, "AuthenticateUser was supposed to try to dial, but didn't") @@ -181,7 +181,7 @@ func TestRealTLSDialing(t *testing.T) { provider := &Provider{ Host: test.host, CABundle: test.caBundle, - Dial: nil, // this test is for the default (production) dialer + Dialer: nil, // this test is for the default (production) dialer } conn, err := provider.dial(test.context) if conn != nil { @@ -198,7 +198,7 @@ func TestRealTLSDialing(t *testing.T) { // Can't test its methods here because we are not dialed to a real LDAP server. require.IsType(t, &ldap.Conn{}, conn) - // Indirectly checking that the Dial method constructed the ldap.Conn with isTLS set to true, + // Indirectly checking that the Dialer method constructed the ldap.Conn with isTLS set to true, // since this is always the correct behavior unless/until we want to support StartTLS. err := conn.(*ldap.Conn).StartTLS(&tls.Config{}) require.EqualError(t, err, `LDAP Result Code 200 "Network Error": ldap: already encrypted`) From 25c1f0d523a6a15652a32502c3f197c44d169948 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 12 Apr 2021 13:53:21 -0700 Subject: [PATCH 11/59] Add Conditions to LDAPIdentityProvider's Status and start to fill them - The ldap_upstream_watcher.go controller validates the bind secret and uses the Conditions to report errors. Shares some condition reporting logic with its sibling controller oidc_upstream_watcher.go, to the extent which is convenient without generics in golang. --- .../types_ldapidentityprovider.go.tmpl | 7 + ...or.pinniped.dev_ldapidentityproviders.yaml | 67 ++++++ generated/1.17/README.adoc | 18 +- .../v1alpha1/types_ldapidentityprovider.go | 7 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 67 ++++++ generated/1.18/README.adoc | 18 +- .../v1alpha1/types_ldapidentityprovider.go | 7 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 67 ++++++ generated/1.19/README.adoc | 18 +- .../v1alpha1/types_ldapidentityprovider.go | 7 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 67 ++++++ generated/1.20/README.adoc | 18 +- .../v1alpha1/types_ldapidentityprovider.go | 7 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 67 ++++++ .../v1alpha1/types_ldapidentityprovider.go | 7 + .../idp/v1alpha1/zz_generated.deepcopy.go | 9 +- .../upstreamwatcher/ldap_upstream_watcher.go | 77 ++++++- .../ldap_upstream_watcher_test.go | 196 ++++++++++++++---- .../upstreamwatcher/oidc_upstream_watcher.go | 41 ++-- .../oidc_upstream_watcher_test.go | 6 +- 24 files changed, 732 insertions(+), 82 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index 5e602f31..1550f0c7 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -26,6 +26,13 @@ type LDAPIdentityProviderStatus struct { // +kubebuilder:default=Pending // +kubebuilder:validation:Enum=Pending;Ready;Error Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } type LDAPIdentityProviderTLSSpec struct { diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 1e54d043..db84c861 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -131,6 +131,73 @@ spec: status: description: Status of the identity provider. properties: + conditions: + description: Represents the observations of an identity provider's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map phase: default: Pending description: Phase summarizes the overall status of the LDAPIdentityProvider. diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index dec89a34..485b3f96 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -669,10 +669,11 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity pro [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-condition"] ==== Condition -Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus[$$OIDCIdentityProviderStatus$$] **** @@ -688,6 +689,18 @@ Condition status of a resource (mirrored from the metav1.Condition type added in |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-conditionstatus"] +==== ConditionStatus (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -761,6 +774,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. |=== @@ -927,7 +941,7 @@ Status of an OIDC identity provider. |=== | Field | Description | *`phase`* __OIDCIdentityProviderPhase__ | Phase summarizes the overall status of the OIDCIdentityProvider. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-condition[$$Condition$$]__ | Represents the observations of an identity provider's current state. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 5e602f31..1550f0c7 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -26,6 +26,13 @@ type LDAPIdentityProviderStatus struct { // +kubebuilder:default=Pending // +kubebuilder:validation:Enum=Pending;Ready;Error Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } type LDAPIdentityProviderTLSSpec struct { diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 6ddcebad..1b19762c 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -34,7 +34,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -131,6 +131,13 @@ func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 1e54d043..db84c861 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -131,6 +131,73 @@ spec: status: description: Status of the identity provider. properties: + conditions: + description: Represents the observations of an identity provider's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map phase: default: Pending description: Phase summarizes the overall status of the LDAPIdentityProvider. diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 6609724e..57bfe7d2 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -669,10 +669,11 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity pro [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-condition"] ==== Condition -Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus[$$OIDCIdentityProviderStatus$$] **** @@ -688,6 +689,18 @@ Condition status of a resource (mirrored from the metav1.Condition type added in |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-conditionstatus"] +==== ConditionStatus (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -761,6 +774,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. |=== @@ -927,7 +941,7 @@ Status of an OIDC identity provider. |=== | Field | Description | *`phase`* __OIDCIdentityProviderPhase__ | Phase summarizes the overall status of the OIDCIdentityProvider. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-condition[$$Condition$$]__ | Represents the observations of an identity provider's current state. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 5e602f31..1550f0c7 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -26,6 +26,13 @@ type LDAPIdentityProviderStatus struct { // +kubebuilder:default=Pending // +kubebuilder:validation:Enum=Pending;Ready;Error Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } type LDAPIdentityProviderTLSSpec struct { diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 6ddcebad..1b19762c 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -34,7 +34,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -131,6 +131,13 @@ func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 1e54d043..db84c861 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -131,6 +131,73 @@ spec: status: description: Status of the identity provider. properties: + conditions: + description: Represents the observations of an identity provider's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map phase: default: Pending description: Phase summarizes the overall status of the LDAPIdentityProvider. diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index e9c1538e..db1c1590 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -669,10 +669,11 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity pro [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-condition"] ==== Condition -Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus[$$OIDCIdentityProviderStatus$$] **** @@ -688,6 +689,18 @@ Condition status of a resource (mirrored from the metav1.Condition type added in |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-conditionstatus"] +==== ConditionStatus (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -761,6 +774,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. |=== @@ -927,7 +941,7 @@ Status of an OIDC identity provider. |=== | Field | Description | *`phase`* __OIDCIdentityProviderPhase__ | Phase summarizes the overall status of the OIDCIdentityProvider. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-condition[$$Condition$$]__ | Represents the observations of an identity provider's current state. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 5e602f31..1550f0c7 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -26,6 +26,13 @@ type LDAPIdentityProviderStatus struct { // +kubebuilder:default=Pending // +kubebuilder:validation:Enum=Pending;Ready;Error Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } type LDAPIdentityProviderTLSSpec struct { diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 6ddcebad..1b19762c 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -34,7 +34,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -131,6 +131,13 @@ func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 1e54d043..db84c861 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -131,6 +131,73 @@ spec: status: description: Status of the identity provider. properties: + conditions: + description: Represents the observations of an identity provider's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map phase: default: Pending description: Phase summarizes the overall status of the LDAPIdentityProvider. diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index f49bf630..25cfd6ff 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -669,10 +669,11 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity pro [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-condition"] ==== Condition -Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderstatus[$$LDAPIdentityProviderStatus$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcidentityproviderstatus[$$OIDCIdentityProviderStatus$$] **** @@ -688,6 +689,18 @@ Condition status of a resource (mirrored from the metav1.Condition type added in |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-conditionstatus"] +==== ConditionStatus (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] +**** + + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovider"] ==== LDAPIdentityProvider @@ -761,6 +774,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`phase`* __LDAPIdentityProviderPhase__ | Phase summarizes the overall status of the LDAPIdentityProvider. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. |=== @@ -927,7 +941,7 @@ Status of an OIDC identity provider. |=== | Field | Description | *`phase`* __OIDCIdentityProviderPhase__ | Phase summarizes the overall status of the OIDCIdentityProvider. -| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-condition[$$Condition$$]__ | Represents the observations of an identity provider's current state. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an identity provider's current state. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 5e602f31..1550f0c7 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -26,6 +26,13 @@ type LDAPIdentityProviderStatus struct { // +kubebuilder:default=Pending // +kubebuilder:validation:Enum=Pending;Ready;Error Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } type LDAPIdentityProviderTLSSpec struct { diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 6ddcebad..1b19762c 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -34,7 +34,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -131,6 +131,13 @@ func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 1e54d043..db84c861 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -131,6 +131,73 @@ spec: status: description: Status of the identity provider. properties: + conditions: + description: Represents the observations of an identity provider's + current state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map phase: default: Pending description: Phase summarizes the overall status of the LDAPIdentityProvider. diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 5e602f31..1550f0c7 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -26,6 +26,13 @@ type LDAPIdentityProviderStatus struct { // +kubebuilder:default=Pending // +kubebuilder:validation:Enum=Pending;Ready;Error Phase LDAPIdentityProviderPhase `json:"phase,omitempty"` + + // Represents the observations of an identity provider's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } type LDAPIdentityProviderTLSSpec struct { diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 6ddcebad..1b19762c 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -34,7 +34,7 @@ func (in *LDAPIdentityProvider) DeepCopyInto(out *LDAPIdentityProvider) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -131,6 +131,13 @@ func (in *LDAPIdentityProviderSpec) DeepCopy() *LDAPIdentityProviderSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderStatus) DeepCopyInto(out *LDAPIdentityProviderStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index 207d7a79..ae214c23 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -4,11 +4,15 @@ package upstreamwatcher import ( + "context" "fmt" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" corev1informers "k8s.io/client-go/informers/core/v1" + "k8s.io/klog/v2/klogr" "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" @@ -22,6 +26,9 @@ import ( const ( ldapControllerName = "ldap-upstream-observer" ldapBindAccountSecretType = corev1.SecretTypeBasicAuth + + // Constants related to conditions. + typeBindSecretValid = "BindSecretValid" ) // UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. @@ -78,7 +85,7 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { requeue := false validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams)) for _, upstream := range actualUpstreams { - valid := c.validateUpstream(upstream) + valid := c.validateUpstream(ctx.Context, upstream) if valid == nil { requeue = true } else { @@ -92,7 +99,7 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { return nil } -func (c *ldapWatcherController) validateUpstream(upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI { +func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI { spec := upstream.Spec result := &upstreamldap.Provider{ Name: upstream.Name, @@ -106,7 +113,13 @@ func (c *ldapWatcherController) validateUpstream(upstream *v1alpha1.LDAPIdentity }, Dialer: c.ldapDialer, } - _ = c.validateSecret(upstream, result) + conditions := []*v1alpha1.Condition{ + c.validateSecret(upstream, result), + } + hadErrorCondition := c.updateStatus(ctx, upstream, conditions) + if hadErrorCondition { + return nil + } return result } @@ -115,22 +128,64 @@ func (c ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPro secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) if err != nil { - // TODO - return nil + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonNotFound, + Message: err.Error(), + } } if secret.Type != corev1.SecretTypeBasicAuth { - // TODO - return nil + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonWrongType, + Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", secretName, secret.Type, corev1.SecretTypeBasicAuth), + } } result.BindUsername = string(secret.Data[corev1.BasicAuthUsernameKey]) result.BindPassword = string(secret.Data[corev1.BasicAuthPasswordKey]) if len(result.BindUsername) == 0 || len(result.BindPassword) == 0 { - // TODO - return nil + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingKeys, + Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", secretName, []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey}), + } } - var cond *v1alpha1.Condition // satisfy linter - return cond + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + Status: v1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: "loaded bind secret", + } +} + +func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) bool { + log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) + updated := upstream.DeepCopy() + + hadErrorCondition := mergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) + + updated.Status.Phase = v1alpha1.LDAPPhaseReady + if hadErrorCondition { + updated.Status.Phase = v1alpha1.LDAPPhaseError + } + + if equality.Semantic.DeepEqual(upstream, updated) { + return hadErrorCondition + } + + _, err := c.client. + IDPV1alpha1(). + LDAPIdentityProviders(upstream.Namespace). + UpdateStatus(ctx, updated, metav1.UpdateOptions{}) + if err != nil { + log.Error(err, "failed to update status") + } + + return hadErrorCondition } diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index e7f9f503..79951433 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -5,7 +5,9 @@ package upstreamwatcher import ( "context" + "fmt" "testing" + "time" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -138,11 +140,12 @@ func (d *comparableDialer) Dial(ctx context.Context, hostAndPort string) (upstre func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { t.Parallel() + now := metav1.NewTime(time.Now().UTC()) const ( testNamespace = "test-namespace" testName = "test-name" - testSecretName = "test-client-secret" + testSecretName = "test-bind-secret" testBindUsername = "test-bind-username" testBindPassword = "test-bind-password" testHost = "ldap.example.com:123" @@ -163,6 +166,38 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, } + validUpstream := &v1alpha1.LDAPIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, + Spec: v1alpha1.LDAPIdentityProviderSpec{ + Host: testHost, + TLS: &v1alpha1.LDAPIdentityProviderTLSSpec{CertificateAuthorityData: testCABundle}, + Bind: v1alpha1.LDAPIdentityProviderBindSpec{SecretName: testSecretName}, + UserSearch: v1alpha1.LDAPIdentityProviderUserSearchSpec{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + Attributes: v1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ + Username: testUsernameAttrName, + UniqueID: testUIDAttrName, + }, + }, + }, + } + + providerForValidUpstream := &upstreamldap.Provider{ + Name: testName, + Host: testHost, + CABundle: []byte(testCABundle), + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: &upstreamldap.UserSearch{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through + } + tests := []struct { name string inputUpstreams []runtime.Object @@ -176,50 +211,108 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { name: "no LDAPIdentityProvider upstreams clears the cache", }, { - name: "one valid upstream updates the cache to include only that upstream", - ldapDialer: successfulDialer, - inputUpstreams: []runtime.Object{&v1alpha1.LDAPIdentityProvider{ - ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, - Spec: v1alpha1.LDAPIdentityProviderSpec{ - Host: testHost, - TLS: &v1alpha1.LDAPIdentityProviderTLSSpec{CertificateAuthorityData: testCABundle}, - Bind: v1alpha1.LDAPIdentityProviderBindSpec{SecretName: testSecretName}, - UserSearch: v1alpha1.LDAPIdentityProviderUserSearchSpec{ - Base: testUserSearchBase, - Filter: testUserSearchFilter, - Attributes: v1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ - Username: testUsernameAttrName, - UniqueID: testUIDAttrName, - }, - }, - }, - }}, + name: "one valid upstream updates the cache to include only that upstream", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{validUpstream}, inputSecrets: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, - wantResultingCache: []*upstreamldap.Provider{ - { - Name: testName, - Host: testHost, - CABundle: []byte(testCABundle), - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: &upstreamldap.UserSearch{ - Base: testUserSearchBase, - Filter: testUserSearchFilter, - UsernameAttribute: testUsernameAttrName, - UIDAttribute: testUIDAttrName, - }, - Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through - }, - }, + wantResultingCache: []*upstreamldap.Provider{providerForValidUpstream}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Ready", - // TODO Conditions + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, + { + name: "missing secret", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{validUpstream}, + inputSecrets: []runtime.Object{}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.Provider{}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "False", + LastTransitionTime: now, + Reason: "SecretNotFound", + Message: fmt.Sprintf(`secret "%s" not found`, testSecretName), + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, + { + name: "secret has wrong type", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{validUpstream}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: "some-other-type", + Data: testValidSecretData, + }}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.Provider{}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "False", + LastTransitionTime: now, + Reason: "SecretWrongType", + Message: fmt.Sprintf(`referenced Secret "%s" has wrong type "some-other-type" (should be "kubernetes.io/basic-auth")`, testSecretName), + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, + { + name: "secret is missing key", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{validUpstream}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + }}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.Provider{}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "False", + LastTransitionTime: now, + Reason: "SecretMissingKeys", + Message: fmt.Sprintf(`referenced Secret "%s" is missing required keys ["username" "password"]`, testSecretName), + ObservedGeneration: 1234, + }, + }, }, }}, }, @@ -271,9 +364,13 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) require.NoError(t, err) - // TODO maybe use something like the normalizeUpstreams() helper to make assertions about what was updated - _ = actualUpstreams - // require.ElementsMatch(t, tt.wantResultingUpstreams, actualUpstreams.Items) + // Assert on the expected Status of the upstreams. Preprocess the upstreams a bit so that they're easier to assert against. + normalizedActualUpstreams := normalizeLDAPUpstreams(actualUpstreams.Items, now) + require.Equal(t, len(tt.wantResultingUpstreams), len(normalizedActualUpstreams)) + for i := range tt.wantResultingUpstreams { + // Require each separately to get a nice diff when the test fails. + require.Equal(t, tt.wantResultingUpstreams[i], normalizedActualUpstreams[i]) + } // Running the sync() a second time should be idempotent, and should return the same error. if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { @@ -284,3 +381,24 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }) } } + +func normalizeLDAPUpstreams(upstreams []v1alpha1.LDAPIdentityProvider, now metav1.Time) []v1alpha1.LDAPIdentityProvider { + result := make([]v1alpha1.LDAPIdentityProvider, 0, len(upstreams)) + for _, u := range upstreams { + normalized := u.DeepCopy() + + // We're only interested in comparing the status, so zero out the spec. + normalized.Spec = v1alpha1.LDAPIdentityProviderSpec{} + + // Round down the LastTransitionTime values to `now` if they were just updated. This makes + // it much easier to encode assertions about the expected timestamps. + for i := range normalized.Status.Conditions { + if time.Since(normalized.Status.Conditions[i].LastTransitionTime.Time) < 5*time.Second { + normalized.Status.Conditions[i].LastTransitionTime = now + } + } + result = append(result, *normalized) + } + + return result +} diff --git a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go index cbf2690f..7fa0d541 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go @@ -338,24 +338,13 @@ func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1al log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() + hadErrorCondition := mergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) + updated.Status.Phase = v1alpha1.PhaseReady - - for i := range conditions { - cond := conditions[i].DeepCopy() - cond.LastTransitionTime = metav1.Now() - cond.ObservedGeneration = upstream.Generation - if c.mergeCondition(&updated.Status.Conditions, cond) { - log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) - } - if cond.Status == v1alpha1.ConditionFalse { - updated.Status.Phase = v1alpha1.PhaseError - } + if hadErrorCondition { + updated.Status.Phase = v1alpha1.PhaseError } - sort.SliceStable(updated.Status.Conditions, func(i, j int) bool { - return updated.Status.Conditions[i].Type < updated.Status.Conditions[j].Type - }) - if equality.Semantic.DeepEqual(upstream, updated) { return } @@ -369,9 +358,29 @@ func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1al } } +// mergeConditions merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. +func mergeConditions(conditions []*v1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]v1alpha1.Condition, log logr.Logger) bool { + hadErrorCondition := false + for i := range conditions { + cond := conditions[i].DeepCopy() + cond.LastTransitionTime = metav1.Now() + cond.ObservedGeneration = observedGeneration + if mergeCondition(conditionsToUpdate, cond) { + log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) + } + if cond.Status == v1alpha1.ConditionFalse { + hadErrorCondition = true + } + } + sort.SliceStable(*conditionsToUpdate, func(i, j int) bool { + return (*conditionsToUpdate)[i].Type < (*conditionsToUpdate)[j].Type + }) + return hadErrorCondition +} + // mergeCondition merges a new v1alpha1.Condition into a slice of existing conditions. It returns true // if the condition has meaningfully changed. -func (*oidcWatcherController) mergeCondition(existing *[]v1alpha1.Condition, new *v1alpha1.Condition) bool { +func mergeCondition(existing *[]v1alpha1.Condition, new *v1alpha1.Condition) bool { // Find any existing condition with a matching type. var old *v1alpha1.Condition for i := range *existing { diff --git a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go index 35ffcd99..44b2a57e 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go @@ -655,8 +655,8 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().OIDCIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) require.NoError(t, err) - // Preprocess the set of upstreams a bit so that they're easier to assert against. - require.ElementsMatch(t, tt.wantResultingUpstreams, normalizeUpstreams(actualUpstreams.Items, now)) + // Assert on the expected Status of the upstreams. Preprocess the upstreams a bit so that they're easier to assert against. + require.ElementsMatch(t, tt.wantResultingUpstreams, normalizeOIDCUpstreams(actualUpstreams.Items, now)) // Running the sync() a second time should be idempotent except for logs, and should return the same error. // This also helps exercise code paths where the OIDC provider discovery hits cache. @@ -669,7 +669,7 @@ func TestOIDCUpstreamWatcherControllerSync(t *testing.T) { } } -func normalizeUpstreams(upstreams []v1alpha1.OIDCIdentityProvider, now metav1.Time) []v1alpha1.OIDCIdentityProvider { +func normalizeOIDCUpstreams(upstreams []v1alpha1.OIDCIdentityProvider, now metav1.Time) []v1alpha1.OIDCIdentityProvider { result := make([]v1alpha1.OIDCIdentityProvider, 0, len(upstreams)) for _, u := range upstreams { normalized := u.DeepCopy() From e24d5891dd79285a4a72f5b79b04798f777ccae8 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 12 Apr 2021 14:12:51 -0700 Subject: [PATCH 12/59] ldap_upstream_watcher_test.go: add another unit test --- .../ldap_upstream_watcher_test.go | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index 79951433..6f836fe6 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -6,6 +6,7 @@ package upstreamwatcher import ( "context" "fmt" + "sort" "testing" "time" @@ -182,6 +183,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, } + modifiedCopyOfValidUpstream := func(editFunc func(*v1alpha1.LDAPIdentityProvider)) *v1alpha1.LDAPIdentityProvider { + deepCopy := validUpstream.DeepCopy() + editFunc(deepCopy) + return deepCopy + } providerForValidUpstream := &upstreamldap.Provider{ Name: testName, @@ -316,6 +322,56 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }}, }, + { + name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{validUpstream, modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Name = "other-upstream" + upstream.Generation = 42 + upstream.Spec.Bind.SecretName = "non-existent-secret" + })}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.Provider{providerForValidUpstream}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{ + { + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "False", + LastTransitionTime: now, + Reason: "SecretNotFound", + Message: fmt.Sprintf(`secret "%s" not found`, "non-existent-secret"), + ObservedGeneration: 42, + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: 1234, + }, + }, + }, + }, + }, + }, } for _, tt := range tests { tt := tt @@ -400,5 +456,9 @@ func normalizeLDAPUpstreams(upstreams []v1alpha1.LDAPIdentityProvider, now metav result = append(result, *normalized) } + sort.SliceStable(result, func(i, j int) bool { + return result[i].Name < result[j].Name + }) + return result } From f0c4305e53c840731a2df6988c3f4bfb28421373 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 12 Apr 2021 17:50:25 -0700 Subject: [PATCH 13/59] Started implementation of LDAP user search and bind --- internal/upstreamldap/upstreamldap.go | 164 +++++++++-- internal/upstreamldap/upstreamldap_test.go | 301 +++++++++++++++++---- 2 files changed, 389 insertions(+), 76 deletions(-) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 8e62f724..eca83707 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -14,10 +14,13 @@ import ( "github.com/go-ldap/ldap/v3" "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" ) const ( - ldapsScheme = "ldaps" + ldapsScheme = "ldaps" + distinguishedNameAttributeName = "dn" + userSearchFilterInterpolationLocationMarker = "{}" ) // Conn abstracts the upstream LDAP communication protocol (mostly for testing). @@ -158,25 +161,152 @@ func (p *Provider) GetURL() string { return fmt.Sprintf("%s://%s", ldapsScheme, p.Host) } -// TestConnection provides a method for testing the connection and bind settings by dialing and binding. -func (p *Provider) TestConnection(ctx context.Context) error { +// TestConnection provides a method for testing the connection and bind settings. It performs a dial and bind +// and returns any errors that we encountered. +func (p *Provider) TestConnection(ctx context.Context) (*authenticator.Response, error) { _, _ = p.dial(ctx) - // TODO bind using the bind credentials - // TODO close - // TODO return any dial or bind errors - return nil + // TODO implement me + return nil, nil +} + +// TestAuthenticateUser provides a method for testing all of the Provider settings in a kind of dry run of +// authentication. 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 authenticator.Response values and the same errors that a real call to +// AuthenticateUser with the correct password would return. +func (p *Provider) TestAuthenticateUser(ctx context.Context, testUsername string) (*authenticator.Response, error) { + // TODO implement me + return nil, nil } // Authenticate a 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) { - _, _ = p.dial(ctx) - // TODO bind - // TODO user search - // TODO user bind - // TODO map username and uid attributes - // TODO group search - // TODO map group attributes - // TODO close - // TODO return any errors that were encountered along the way - return nil, false, nil + conn, err := p.dial(ctx) + if err != nil { + return nil, false, fmt.Errorf(`error dialing host "%s": %w`, p.Host, err) + } + defer conn.Close() + + err = conn.Bind(p.BindUsername, p.BindPassword) + if err != nil { + // TODO test this + return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.BindUsername, err) + } + + mappedUsername, mappedUID, err := p.searchAndBindUser(conn, username, password) + if err != nil { + return nil, false, err + } + + response := &authenticator.Response{ + User: &user.DefaultInfo{ + Name: mappedUsername, + UID: mappedUID, + Groups: []string{}, // Support for group search coming soon. + }, + } + return response, true, nil +} + +func (p *Provider) searchAndBindUser(conn Conn, username string, password string) (string, string, error) { + searchResult, err := conn.Search(p.userSearchRequest(username)) + if err != nil { + // TODO test this + return "", "", fmt.Errorf(`error searching for user "%s": %w`, username, err) + } + if len(searchResult.Entries) != 1 { + // TODO test this + return "", "", 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 { + // TODO test this + return "", "", fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username) + } + + mappedUsername, err := p.getSearchResultAttributeValue(p.UserSearch.UsernameAttribute, userEntry, username) + if err != nil { + // TODO test this + return "", "", err + } + + mappedUID, err := p.getSearchResultAttributeValue(p.UserSearch.UIDAttribute, userEntry, username) + if err != nil { + // TODO test this + return "", "", err + } + + // Take care that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername! + err = conn.Bind(userEntry.DN, password) + if err != nil { + // TODO test this + return "", "", fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) + } + + return mappedUsername, mappedUID, nil +} + +func (p *Provider) userSearchRequest(username string) *ldap.SearchRequest { + // See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options. + return &ldap.SearchRequest{ + BaseDN: p.UserSearch.Base, + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.DerefAlways, // TODO what's the best value here? + SizeLimit: 2, + TimeLimit: 90, + TypesOnly: false, + Filter: p.userSearchFilter(username), + 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.UserSearch.UsernameAttribute != distinguishedNameAttributeName { + attributes = append(attributes, p.UserSearch.UsernameAttribute) + } + if p.UserSearch.UIDAttribute != distinguishedNameAttributeName { + attributes = append(attributes, p.UserSearch.UIDAttribute) + } + return attributes +} + +func (p *Provider) userSearchFilter(username string) string { + safeUsername := p.escapeUsernameForSearchFilter(username) + if len(p.UserSearch.Filter) == 0 { + return fmt.Sprintf("%s=%s", p.UserSearch.UsernameAttribute, safeUsername) + } + return strings.ReplaceAll(p.UserSearch.Filter, userSearchFilterInterpolationLocationMarker, safeUsername) +} + +func (p *Provider) escapeUsernameForSearchFilter(username string) string { + // The username is end-user input, so it should be escaped before being included in a search to prevent query injection. + return ldap.EscapeFilter(username) +} + +func (p *Provider) getSearchResultAttributeValue(attributeName string, fromUserEntry *ldap.Entry, username string) (string, error) { + if attributeName == distinguishedNameAttributeName { + return fromUserEntry.DN, nil + } + + attributeValues := fromUserEntry.GetAttributeValues(attributeName) + + if len(attributeValues) != 1 { + // TODO test this + return "", fmt.Errorf(`found %d values for attribute "%s" while searching for user "%s", but expected 1 result`, + len(attributeValues), attributeName, username, + ) + } + + attributeValue := attributeValues[0] + if len(attributeValue) == 0 { + // TODO test this + return "", fmt.Errorf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, + attributeName, username, + ) + } + + return attributeValue, nil } diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 358582f7..9b17c537 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -6,6 +6,7 @@ package upstreamldap import ( "context" "crypto/tls" + "errors" "fmt" "net" "net/http" @@ -22,92 +23,274 @@ import ( "go.pinniped.dev/internal/testutil" ) +const ( + testHost = "ldap.example.com:8443" + testBindUsername = "some-bind-username" + testBindPassword = "some-bind-password" + testUpstreamUsername = "some-upstream-username" + testUpstreamPassword = "some-upstream-password" + testUserSearchBase = "some-upstream-base-dn" + testUserSearchFilter = "some-filter={}-and-more-filter={}" + testUserSearchUsernameAttribute = "some-upstream-username-attribute" + testUserSearchUIDAttribute = "some-upstream-uid-attribute" + testSearchResultDNValue = "some-upstream-user-dn" + testSearchResultUsernameAttributeValue = "some-upstream-username-value" + testSearchResultUIDAttributeValue = "some-upstream-uid-value" +) + var ( - upstreamUsername = "some-upstream-username" - upstreamPassword = "some-upstream-password" - upstreamGroups = []string{"some-upstream-group-0", "some-upstream-group-1"} - upstreamUID = "some-upstream-uid" + testUserSearchFilterInterpolated = fmt.Sprintf("some-filter=%s-and-more-filter=%s", testUpstreamUsername, testUpstreamUsername) ) func TestAuthenticateUser(t *testing.T) { - // Please the linter... - _ = upstreamGroups - _ = upstreamUID - t.Skip("TODO: make me pass!") + provider := func(editFunc func(p *Provider)) *Provider { + provider := &Provider{ + Host: testHost, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: &UserSearch{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUserSearchUsernameAttribute, + UIDAttribute: testUserSearchUIDAttribute, + }, + } + if editFunc != nil { + editFunc(provider) + } + return provider + } + + expectedSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { + request := &ldap.SearchRequest{ + BaseDN: testUserSearchBase, + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.DerefAlways, + SizeLimit: 2, + TimeLimit: 90, + TypesOnly: false, + Filter: testUserSearchFilterInterpolated, + Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute}, + Controls: nil, + } + if editFunc != nil { + editFunc(request) + } + return request + } tests := []struct { - name string - provider *Provider - wantError string - wantUnauthenticated bool - wantAuthResponse *authenticator.Response + name string + username string + password string + provider *Provider + setupMocks func(conn *mockldapconn.MockConn) + dialError error + wantError string + wantAuthResponse *authenticator.Response }{ { - name: "happy path", - provider: &Provider{ - Host: "ldap.example.com:8443", - BindUsername: upstreamUsername, - BindPassword: upstreamPassword, - UserSearch: &UserSearch{ - Base: "some-upstream-base-dn", - Filter: "some-filter", - UsernameAttribute: "some-upstream-username-attribute", - UIDAttribute: "some-upstream-uid-attribute", - }, + name: "happy path", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, // TODO are there any response controls that we need to be able to handle? + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + conn.EXPECT().Close().Times(1) }, wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ - Name: upstreamUsername, - Groups: upstreamGroups, - UID: upstreamUID, + Name: testSearchResultUsernameAttributeValue, + Groups: []string{}, // We don't support group search yet. Coming soon! + UID: testSearchResultUIDAttributeValue, }, }, }, + { + name: "when the UsernameAttribute is dn", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(func(p *Provider) { + p.UserSearch.UsernameAttribute = "dn" + }), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{testUserSearchUIDAttribute} + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: testSearchResultDNValue, + Groups: []string{}, // We don't support group search yet. Coming soon! + UID: testSearchResultUIDAttributeValue, + }, + }, + }, + { + name: "when the UIDAttribute is dn", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(func(p *Provider) { + p.UserSearch.UIDAttribute = "dn" + }), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{testUserSearchUsernameAttribute} + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: testSearchResultUsernameAttributeValue, + Groups: []string{}, // We don't support group search yet. Coming soon! + UID: testSearchResultDNValue, + }, + }, + }, + { + name: "when Filter is blank it derives a search filter from the UsernameAttribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(func(p *Provider) { + p.UserSearch.Filter = "" + }), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { + r.Filter = testUserSearchUsernameAttribute + "=" + testUpstreamUsername + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: testSearchResultUsernameAttributeValue, + Groups: []string{}, // We don't support group search yet. Coming soon! + UID: testSearchResultUIDAttributeValue, + }, + }, + }, + { + name: "when the username has special LDAP search filter characters then they must be properly escaped in the search filter", + username: `a&b|c(d)e\f*g`, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { + r.Filter = fmt.Sprintf("some-filter=%s-and-more-filter=%s", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: testSearchResultUsernameAttributeValue, + Groups: []string{}, // We don't support group search yet. Coming soon! + UID: testSearchResultUIDAttributeValue, + }, + }, + }, + // TODO are LDAP attribute names case sensitive? do we need any special handling for case? + { + name: "when dial fails", + provider: provider(nil), + dialError: errors.New("some dial error"), + wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost), + }, } + for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { + tt := test + t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) + conn := mockldapconn.NewMockConn(ctrl) - conn.EXPECT().Bind(test.provider.BindUsername, test.provider.BindPassword).Times(1) - conn.EXPECT().Search(&ldap.SearchRequest{ - BaseDN: test.provider.UserSearch.Base, - Scope: 99, // TODO: what should this be? - DerefAliases: 99, // TODO: what should this be? - SizeLimit: 99, - TimeLimit: 99, // TODO: what should this be? - TypesOnly: true, // TODO: what should this be? - Filter: test.provider.UserSearch.Filter, - Attributes: []string{}, // TODO: what should this be? - Controls: []ldap.Control{}, // TODO: what should this be? - }).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: "", // TODO: what should this be? - Attributes: []*ldap.EntryAttribute{}, // TODO: what should this be? - }, - }, - Referrals: []string{}, // TODO: what should this be? - Controls: []ldap.Control{}, // TODO: what should this be? - }, nil).Times(1) - conn.EXPECT().Close().Times(1) + if tt.setupMocks != nil { + tt.setupMocks(conn) + } dialWasAttempted := false - test.provider.Dialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) { + tt.provider.Dialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) { dialWasAttempted = true - require.Equal(t, test.provider.Host, hostAndPort) + require.Equal(t, tt.provider.Host, hostAndPort) + if tt.dialError != nil { + return nil, tt.dialError + } return conn, nil }) - authResponse, authenticated, err := test.provider.AuthenticateUser(context.Background(), upstreamUsername, upstreamPassword) + authResponse, authenticated, err := tt.provider.AuthenticateUser(context.Background(), tt.username, tt.password) + require.True(t, dialWasAttempted, "AuthenticateUser was supposed to try to dial, but didn't") - if test.wantError != "" { - require.EqualError(t, err, test.wantError) - return + + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + require.False(t, authenticated) + require.Nil(t, authResponse) + } else { + require.NoError(t, err) + require.True(t, authenticated) + require.Equal(t, tt.wantAuthResponse, authResponse) } - require.Equal(t, !test.wantUnauthenticated, authenticated) - require.Equal(t, test.wantAuthResponse, authResponse) }) } } From 7b8c86b38ee69a68d279f720e996b1bc547175f4 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 13 Apr 2021 08:38:04 -0700 Subject: [PATCH 14/59] Handle error cases during LDAP user search and bind --- internal/upstreamldap/upstreamldap.go | 9 - internal/upstreamldap/upstreamldap_test.go | 227 +++++++++++++++++++++ 2 files changed, 227 insertions(+), 9 deletions(-) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index eca83707..b5d79c80 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -188,7 +188,6 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri err = conn.Bind(p.BindUsername, p.BindPassword) if err != nil { - // TODO test this return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.BindUsername, err) } @@ -210,37 +209,31 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri func (p *Provider) searchAndBindUser(conn Conn, username string, password string) (string, string, error) { searchResult, err := conn.Search(p.userSearchRequest(username)) if err != nil { - // TODO test this return "", "", fmt.Errorf(`error searching for user "%s": %w`, username, err) } if len(searchResult.Entries) != 1 { - // TODO test this return "", "", 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 { - // TODO test this return "", "", fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username) } mappedUsername, err := p.getSearchResultAttributeValue(p.UserSearch.UsernameAttribute, userEntry, username) if err != nil { - // TODO test this return "", "", err } mappedUID, err := p.getSearchResultAttributeValue(p.UserSearch.UIDAttribute, userEntry, username) if err != nil { - // TODO test this return "", "", err } // Take care that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername! err = conn.Bind(userEntry.DN, password) if err != nil { - // TODO test this return "", "", fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) } @@ -294,7 +287,6 @@ func (p *Provider) getSearchResultAttributeValue(attributeName string, fromUserE attributeValues := fromUserEntry.GetAttributeValues(attributeName) if len(attributeValues) != 1 { - // TODO test this return "", fmt.Errorf(`found %d values for attribute "%s" while searching for user "%s", but expected 1 result`, len(attributeValues), attributeName, username, ) @@ -302,7 +294,6 @@ func (p *Provider) getSearchResultAttributeValue(attributeName string, fromUserE attributeValue := attributeValues[0] if len(attributeValue) == 0 { - // TODO test this return "", fmt.Errorf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, attributeName, username, ) diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 9b17c537..61b2a305 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -255,6 +255,233 @@ func TestAuthenticateUser(t *testing.T) { dialError: errors.New("some dial error"), wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost), }, + { + name: "when binding as the bind user returns an error", + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`error binding as "%s" before user search: some bind error`, testBindUsername), + }, + { + name: "when searching for the user returns an error", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(nil, errors.New("some search error")).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`error searching for user "%s": some search error`, testUpstreamUsername), + }, + { + name: "when searching for the user returns no results", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`searching for user "%s" resulted in 0 search results, but expected 1 result`, testUpstreamUsername), + }, + { + name: "when searching for the user returns multiple results", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + {DN: testSearchResultDNValue}, + {DN: "some-other-dn"}, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`searching for user "%s" resulted in 2 search results, but expected 1 result`, testUpstreamUsername), + }, + { + name: "when searching for the user returns a user without a DN", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + {DN: ""}, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`searching for user "%s" resulted in search result without DN`, testUpstreamUsername), + }, + { + name: "when searching for the user returns a user without an expected username attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`found 0 values for attribute "%s" while searching for user "%s", but expected 1 result`, testUserSearchUsernameAttribute, testUpstreamUsername), + }, + { + name: "when searching for the user returns a user with too many values for the expected username attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{ + testSearchResultUsernameAttributeValue, + "unexpected-additional-value", + }), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`found 2 values for attribute "%s" while searching for user "%s", but expected 1 result`, testUserSearchUsernameAttribute, testUpstreamUsername), + }, + { + name: "when searching for the user returns a user with an empty value for the expected username attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{""}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, testUserSearchUsernameAttribute, testUpstreamUsername), + }, + { + name: "when searching for the user returns a user without an expected UID attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`found 0 values for attribute "%s" while searching for user "%s", but expected 1 result`, testUserSearchUIDAttribute, testUpstreamUsername), + }, + { + name: "when searching for the user returns a user with too many values for the expected UID attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{ + testSearchResultUIDAttributeValue, + "unexpected-additional-value", + }), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`found 2 values for attribute "%s" while searching for user "%s", but expected 1 result`, testUserSearchUIDAttribute, testUpstreamUsername), + }, + { + name: "when searching for the user returns a user with an empty value for the expected UID attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{""}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, testUserSearchUIDAttribute, testUpstreamUsername), + }, + { + name: "when binding as the found user returns an error", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(errors.New("some bind error")).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`error binding for user "%s" using provided password against DN "%s": some bind error`, testUpstreamUsername, testSearchResultDNValue), + }, } for _, test := range tests { From fec3d92f263dabc6525e372b09e0539fdec4303c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 13 Apr 2021 15:23:14 -0700 Subject: [PATCH 15/59] Add integration test for upstreamldap.Provider - The unit tests for upstreamldap.Provider need to mock the LDAP server, so add an integration test which allows us to get fast feedback for this code against a real LDAP server. - Automatically wrap the user search filter in parenthesis if it is not already wrapped in parens. - More special handling for using "dn" as the username or UID attribute name. - Also added some more comments to types_ldapidentityprovider.go.tmpl --- .../types_ldapidentityprovider.go.tmpl | 12 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 26 +- generated/1.17/README.adoc | 6 +- .../v1alpha1/types_ldapidentityprovider.go | 12 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 26 +- generated/1.18/README.adoc | 6 +- .../v1alpha1/types_ldapidentityprovider.go | 12 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 26 +- generated/1.19/README.adoc | 6 +- .../v1alpha1/types_ldapidentityprovider.go | 12 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 26 +- generated/1.20/README.adoc | 6 +- .../v1alpha1/types_ldapidentityprovider.go | 12 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 26 +- .../v1alpha1/types_ldapidentityprovider.go | 12 +- internal/upstreamldap/upstreamldap.go | 13 +- internal/upstreamldap/upstreamldap_test.go | 66 +- test/integration/ldapsearch_test.go | 608 ++++++++++++++++++ 18 files changed, 845 insertions(+), 68 deletions(-) create mode 100644 test/integration/ldapsearch_test.go diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index 1550f0c7..0861c0de 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 485b3f96..8de20adc 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -808,8 +808,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -827,7 +827,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 57bfe7d2..6de19496 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -808,8 +808,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -827,7 +827,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index db1c1590..37d2251c 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -808,8 +808,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -827,7 +827,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 25cfd6ff..a24ce0b5 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -808,8 +808,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -827,7 +827,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index b5d79c80..a5af4fe6 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -180,6 +180,11 @@ func (p *Provider) TestAuthenticateUser(ctx context.Context, testUsername string // Authenticate a 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) { + if p.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.UserSearch.Filter) == 0 { + // LDAP search filters do not allow searching by DN. + return nil, false, fmt.Errorf(`must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`) + } + conn, err := p.dial(ctx) if err != nil { return nil, false, fmt.Errorf(`error dialing host "%s": %w`, p.Host, err) @@ -269,9 +274,13 @@ func (p *Provider) userSearchRequestedAttributes() []string { func (p *Provider) userSearchFilter(username string) string { safeUsername := p.escapeUsernameForSearchFilter(username) if len(p.UserSearch.Filter) == 0 { - return fmt.Sprintf("%s=%s", p.UserSearch.UsernameAttribute, safeUsername) + return fmt.Sprintf("(%s=%s)", p.UserSearch.UsernameAttribute, safeUsername) } - return strings.ReplaceAll(p.UserSearch.Filter, userSearchFilterInterpolationLocationMarker, safeUsername) + filter := strings.ReplaceAll(p.UserSearch.Filter, userSearchFilterInterpolationLocationMarker, safeUsername) + if strings.HasPrefix(filter, "(") && strings.HasSuffix(filter, ")") { + return filter + } + return "(" + filter + ")" } func (p *Provider) escapeUsernameForSearchFilter(username string) string { diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 61b2a305..39b19093 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -25,7 +25,7 @@ import ( const ( testHost = "ldap.example.com:8443" - testBindUsername = "some-bind-username" + testBindUsername = "cn=some-bind-username,dc=pinniped,dc=dev" testBindPassword = "some-bind-password" testUpstreamUsername = "some-upstream-username" testUpstreamPassword = "some-upstream-password" @@ -39,7 +39,7 @@ const ( ) var ( - testUserSearchFilterInterpolated = fmt.Sprintf("some-filter=%s-and-more-filter=%s", testUpstreamUsername, testUpstreamUsername) + testUserSearchFilterInterpolated = fmt.Sprintf("(some-filter=%s-and-more-filter=%s)", testUpstreamUsername, testUpstreamUsername) ) func TestAuthenticateUser(t *testing.T) { @@ -87,6 +87,7 @@ func TestAuthenticateUser(t *testing.T) { setupMocks func(conn *mockldapconn.MockConn) dialError error wantError string + wantToSkipDial bool wantAuthResponse *authenticator.Response }{ { @@ -115,13 +116,44 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultUIDAttributeValue, + Groups: []string{}, }, }, }, { - name: "when the UsernameAttribute is dn", + name: "when the user search filter is already wrapped by parenthesis then it is not wrapped again", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(func(p *Provider) { + p.UserSearch.Filter = "(" + testUserSearchFilter + ")" + }), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: testSearchResultUsernameAttributeValue, + UID: testSearchResultUIDAttributeValue, + Groups: []string{}, + }, + }, + }, + { + name: "when the UsernameAttribute is dn and there is a user search filter provided", username: testUpstreamUsername, password: testUpstreamPassword, provider: provider(func(p *Provider) { @@ -147,8 +179,8 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultDNValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultUIDAttributeValue, + Groups: []string{}, }, }, }, @@ -179,8 +211,8 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultDNValue, + Groups: []string{}, }, }, }, @@ -194,7 +226,7 @@ func TestAuthenticateUser(t *testing.T) { setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { - r.Filter = testUserSearchUsernameAttribute + "=" + testUpstreamUsername + r.Filter = "(" + testUserSearchUsernameAttribute + "=" + testUpstreamUsername + ")" })).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -212,8 +244,8 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultUIDAttributeValue, + Groups: []string{}, }, }, }, @@ -225,7 +257,7 @@ func TestAuthenticateUser(t *testing.T) { setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { - r.Filter = fmt.Sprintf("some-filter=%s-and-more-filter=%s", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) + r.Filter = fmt.Sprintf("(some-filter=%s-and-more-filter=%s)", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) })).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -243,18 +275,28 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultUIDAttributeValue, + Groups: []string{}, }, }, }, - // TODO are LDAP attribute names case sensitive? do we need any special handling for case? { name: "when dial fails", provider: provider(nil), dialError: errors.New("some dial error"), wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost), }, + { + name: "when the UsernameAttribute is dn and there is not a user search filter provided", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(func(p *Provider) { + p.UserSearch.UsernameAttribute = "dn" + p.UserSearch.Filter = "" + }), + wantToSkipDial: true, + wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`, + }, { name: "when binding as the bind user returns an error", provider: provider(nil), @@ -507,7 +549,7 @@ func TestAuthenticateUser(t *testing.T) { authResponse, authenticated, err := tt.provider.AuthenticateUser(context.Background(), tt.username, tt.password) - require.True(t, dialWasAttempted, "AuthenticateUser was supposed to try to dial, but didn't") + require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) if tt.wantError != "" { require.EqualError(t, err, tt.wantError) diff --git a/test/integration/ldapsearch_test.go b/test/integration/ldapsearch_test.go new file mode 100644 index 00000000..6bd36241 --- /dev/null +++ b/test/integration/ldapsearch_test.go @@ -0,0 +1,608 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package integration + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/exec" + "path" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" + + "go.pinniped.dev/internal/certauthority" + "go.pinniped.dev/internal/upstreamldap" +) + +func TestLDAPSearch(t *testing.T) { + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(func() { + cancelFunc() // this will send SIGKILL to the docker process, just in case + }) + + port := localhostPort(t) + caBundle := dockerRunLDAPServer(ctx, t, port) + + provider := func(editFunc func(p *upstreamldap.Provider)) *upstreamldap.Provider { + provider := &upstreamldap.Provider{ + Name: "test-ldap-provider", + Host: "127.0.0.1:" + port, + CABundle: caBundle, + BindUsername: "cn=admin,dc=pinniped,dc=dev", + BindPassword: "password", + UserSearch: &upstreamldap.UserSearch{ + Base: "ou=users,dc=pinniped,dc=dev", + Filter: "", // defaults to UsernameAttribute={}, i.e. "cn={}" in this case + UsernameAttribute: "cn", + UIDAttribute: "uidNumber", + }, + } + if editFunc != nil { + editFunc(provider) + } + return provider + } + + pinnyPassword := "password123" // from the LDIF file below + wallyPassword := "password456" // from the LDIF file below + + tests := []struct { + name string + username string + password string + provider *upstreamldap.Provider + wantError string + wantAuthResponse *authenticator.Response + }{ + { + name: "happy path", + username: "pinny", + password: pinnyPassword, + provider: provider(nil), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "happy path as a different user", + username: "wally", + password: wallyPassword, + provider: provider(nil), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "wally", UID: "1001", Groups: []string{}}, + }, + }, + { + name: "using a different user search base", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "dc=pinniped,dc=dev" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the user search filter is already wrapped by parenthesis", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "(cn={})" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the UsernameAttribute is dn and a user search filter is provided", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.UsernameAttribute = "dn" + p.UserSearch.Filter = "cn={}" + }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the user search filter allows for different ways of logging in and the first one is used", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.Filter = "(|(cn={})(mail={}))" + }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the user search filter allows for different ways of logging in and the second one is used", + username: "pinny.ldap@example.com", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.Filter = "(|(cn={})(mail={}))" + }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the UIDAttribute is dn", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "dn" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{}}, + }, + }, + { + name: "when the UIDAttribute is sn", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "sn" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{}}, + }, + }, + { + name: "when the UsernameAttribute is sn", + username: "seAl", // note that this is not case-sensitive! sn=Seal + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "sn" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{}}, // note that the final answer is case-sensitive + }, + }, + { + name: "when the UsernameAttribute is dn and there is no user search filter provided", + username: "cn=pinny,ou=users,dc=pinniped,dc=dev", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.UsernameAttribute = "dn" + p.UserSearch.Filter = "" + }), + wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`, + }, + { + name: "when the bind user username is not a valid DN", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.BindUsername = "invalid-dn" }), + wantError: `error binding as "invalid-dn" before user search: LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, + }, + { + name: "when the bind user username is wrong", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.BindUsername = "cn=wrong,dc=pinniped,dc=dev" }), + wantError: `error binding as "cn=wrong,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `, + }, + { + name: "when the bind user password is wrong", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.BindPassword = "wrong-password" }), + wantError: `error binding as "cn=admin,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `, + }, + { + name: "when the end user password is wrong", + username: "pinny", + password: "wrong-pinny-password", + provider: provider(nil), + wantError: `error binding for user "pinny" using provided password against DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 49 "Invalid Credentials": `, + }, + { + name: "when the end user username is wrong", + username: "wrong-username", + password: pinnyPassword, + provider: provider(nil), + wantError: `searching for user "wrong-username" resulted in 0 search results, but expected 1 result`, + }, + { + name: "when the user search filter does not compile", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "*" }), + wantError: `error searching for user "pinny": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`, + }, + { + name: "when there are too many search results for the user", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.Filter = "objectClass=*" // overly broad search filter + }), + wantError: `error searching for user "pinny": LDAP Result Code 4 "Size Limit Exceeded": `, + }, + { + name: "when the server is unreachable", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.Host = "127.0.0.1:27534" }), // hopefully this port is not in use on the host running tests + wantError: `error dialing host "127.0.0.1:27534": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:27534: connect: connection refused`, + }, + { + name: "when the server is not parsable", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.Host = "too:many:ports" }), + wantError: `error dialing host "too:many:ports": LDAP Result Code 200 "Network Error": address too:many:ports: too many colons in address`, + }, + { + name: "when the CA bundle is not parsable", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.CABundle = []byte("invalid-pem") }), + wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, port), + }, + { + name: "when the CA bundle does not cause the host to be trusted", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.CABundle = nil }), + wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, port), + }, + { + name: "when the UsernameAttribute attribute has multiple values in the entry", + username: "wally.ldap@example.com", + password: wallyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "mail" }), + wantError: `found 2 values for attribute "mail" while searching for user "wally.ldap@example.com", but expected 1 result`, + }, + { + name: "when the UIDAttribute attribute has multiple values in the entry", + username: "wally", + password: wallyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "mail" }), + wantError: `found 2 values for attribute "mail" while searching for user "wally", but expected 1 result`, + }, + { + name: "when the UsernameAttribute attribute is not found in the entry", + username: "wally", + password: wallyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.Filter = "cn={}" + p.UserSearch.UsernameAttribute = "attr-does-not-exist" + }), + wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`, + }, + { + name: "when the UIDAttribute attribute is not found in the entry", + username: "wally", + password: wallyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "attr-does-not-exist" }), + wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`, + }, + { + name: "when the UsernameAttribute has the wrong case", + username: "Seal", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "SN" }), // this is case-sensitive + wantError: `found 0 values for attribute "SN" while searching for user "Seal", but expected 1 result`, + }, + { + name: "when the UIDAttribute has the wrong case", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "SN" }), // this is case-sensitive + wantError: `found 0 values for attribute "SN" while searching for user "pinny", but expected 1 result`, + }, + { + name: "when the UsernameAttribute is DN and has the wrong case", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.UsernameAttribute = "DN" // dn must be lower-case + p.UserSearch.Filter = "cn={}" + }), + wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`, + }, + { + name: "when the UIDAttribute is DN and has the wrong case", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.UIDAttribute = "DN" // dn must be lower-case + }), + wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`, + }, + { + name: "when the search base is invalid", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "invalid-base" }), + wantError: `error searching for user "pinny": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, + }, + { + name: "when the search base does not exist", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" }), + wantError: `error searching for user "pinny": LDAP Result Code 32 "No Such Object": `, + }, + { + name: "when the search base causes no search results", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" }), + wantError: `searching for user "pinny" resulted in 0 search results, but expected 1 result`, + }, + { + name: "when there is no username specified", + username: "", + password: pinnyPassword, + provider: provider(nil), + wantError: `searching for user "" resulted in 0 search results, but expected 1 result`, + }, + { + name: "when there is no password specified", + username: "pinny", + password: "", + provider: provider(nil), + wantError: `error binding for user "pinny" using provided password against DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 206 "Empty password not allowed by the client": ldap: empty password not allowed by the client`, + }, + { + name: "when the user has no password in their entry", + username: "olive", + password: "anything", + provider: provider(nil), + wantError: `error binding for user "olive" using provided password against DN "cn=olive,ou=users,dc=pinniped,dc=dev": LDAP Result Code 49 "Invalid Credentials": `, + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + authResponse, authenticated, err := tt.provider.AuthenticateUser(ctx, tt.username, tt.password) + + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + require.False(t, authenticated) + require.Nil(t, authResponse) + } else { + require.NoError(t, err) + require.True(t, authenticated) + require.Equal(t, tt.wantAuthResponse, authResponse) + } + }) + } +} + +func localhostPort(t *testing.T) string { + t.Helper() + + unusedPortGrabbingListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + recentlyClaimedHostAndPort := unusedPortGrabbingListener.Addr().String() + require.NoError(t, unusedPortGrabbingListener.Close()) + + splitHostAndPort := strings.Split(recentlyClaimedHostAndPort, ":") + require.Len(t, splitHostAndPort, 2) + + return splitHostAndPort[1] +} + +func dockerRunLDAPServer(ctx context.Context, t *testing.T, hostPort string) []byte { + t.Helper() + + _, err := exec.LookPath("docker") + require.NoError(t, err) + + ca, err := certauthority.New("Test LDAP CA", time.Hour*24) + require.NoError(t, err) + + certPEM, keyPEM, err := ca.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.1")}, time.Hour*24) + require.NoError(t, err) + + tempDir, err := ioutil.TempDir("", "pinniped-test-*") + require.NoError(t, err) + t.Cleanup(func() { + err := os.Remove(tempDir) + require.NoError(t, err) + }) + + writeToNewTempFile(t, tempDir, "cert.pem", certPEM) + writeToNewTempFile(t, tempDir, "key.pem", keyPEM) + writeToNewTempFile(t, tempDir, "ca.pem", ca.Bundle()) + writeToNewTempFile(t, tempDir, "test.ldif", []byte(testLDIF)) + + dockerArgs := []string{ + "run", + "-e", "BITNAMI_DEBUG=true", + "-e", "LDAP_ADMIN_USERNAME=admin", + "-e", "LDAP_ADMIN_PASSWORD=password", + "-e", "LDAP_ENABLE_TLS=yes", + "-e", "LDAP_TLS_CERT_FILE=/inputs/cert.pem", + "-e", "LDAP_TLS_KEY_FILE=/inputs/key.pem", + "-e", "LDAP_TLS_CA_FILE=/inputs/ca.pem", + "-e", "LDAP_CUSTOM_LDIF_DIR=/inputs", + "-e", "LDAP_ROOT=dc=pinniped,dc=dev", + "-v", tempDir + ":/inputs", + "-p", hostPort + ":1636", + "-m", "64m", + "--rm", // automatically delete the container when finished + "docker.io/bitnami/openldap", + } + + t.Log("Starting:", "docker", strings.Join(dockerArgs, " ")) + + cmd := exec.CommandContext(ctx, "docker", dockerArgs...) + + var stdoutBuf, stderrBuf syncBuffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) + cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + + err = cmd.Start() + require.NoError(t, err) + t.Cleanup(func() { + // docker requires an interrupt signal to end the container. + // This t.Cleanup is registered after the one that cancels the context, so this one will happen first. + err := cmd.Process.Signal(os.Interrupt) + require.NoError(t, err) + time.Sleep(time.Second) // give a moment before we move on, because we'll send SIGKILL in a later t.Cleanup + }) + + earlyTerminationCh := make(chan bool, 1) + go func() { + err = cmd.Wait() + earlyTerminationCh <- true + }() + + terminatedEarly := false + require.Eventually(t, func() bool { + t.Log("Waiting for slapd to start...") + // This substring is contained in the last line of output before the server starts. + if strings.Contains(stderrBuf.String(), " slapd starting\n") { + return true + } + select { + case <-earlyTerminationCh: + terminatedEarly = true + return true + default: // ignore when this non-blocking read found no message + } + return false + }, 2*time.Minute, time.Second) + + require.Falsef(t, terminatedEarly, "docker command ended sooner than expected") + + t.Log("Detected LDAP server has started successfully") + return ca.Bundle() +} + +func writeToNewTempFile(t *testing.T, dir string, filename string, contents []byte) { + t.Helper() + + filePath := path.Join(dir, filename) + + err := ioutil.WriteFile(filePath, contents, 0644) + require.NoError(t, err) + + t.Cleanup(func() { + err := os.Remove(filePath) + require.NoError(t, err) + }) +} + +var testLDIF = ` +# ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! *** +# Here's a good explaination of LDIF: +# https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system + +# pinniped.dev (organization, root) +dn: dc=pinniped,dc=dev +objectClass: dcObject +objectClass: organization +dc: pinniped +o: example + +# users, pinniped.dev (organization unit) +dn: ou=users,dc=pinniped,dc=dev +objectClass: organizationalUnit +ou: users + +# groups, pinniped.dev (organization unit) +dn: ou=groups,dc=pinniped,dc=dev +objectClass: organizationalUnit +ou: groups + +# beach-groups, groups, pinniped.dev (organization unit) +dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev +objectClass: organizationalUnit +ou: beach-groups + +# pinny, users, pinniped.dev (user) +dn: cn=pinny,ou=users,dc=pinniped,dc=dev +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +cn: pinny +sn: Seal +givenName: Pinny +mail: pinny.ldap@example.com +userPassword: password123 +uid: pinny +uidNumber: 1000 +gidNumber: 1000 +homeDirectory: /home/pinny +loginShell: /bin/bash +gecos: pinny-the-seal + +# wally, users, pinniped.dev +dn: cn=wally,ou=users,dc=pinniped,dc=dev +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +cn: wally +sn: Walrus +givenName: Wally +mail: wally.ldap@example.com +mail: wally.alternate@example.com +userPassword: password456 +uid: wally +uidNumber: 1001 +gidNumber: 1001 +homeDirectory: /home/wally +loginShell: /bin/bash +gecos: wally-the-walrus + +# olive, users, pinniped.dev (user without password) +dn: cn=olive,ou=users,dc=pinniped,dc=dev +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +cn: olive +sn: Boston Terrier +givenName: Olive +mail: olive.ldap@example.com +uid: olive +uidNumber: 1002 +gidNumber: 1002 +homeDirectory: /home/olive +loginShell: /bin/bash +gecos: olive-the-dog + +# ball-game-players, beach-groups, groups, pinniped.dev (group of users) +dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev +cn: ball-game-players +objectClass: groupOfNames +member: cn=pinny,ou=users,dc=pinniped,dc=dev +member: cn=olive,ou=users,dc=pinniped,dc=dev + +# seals, groups, pinniped.dev (group of users) +dn: cn=seals,ou=groups,dc=pinniped,dc=dev +cn: seals +objectClass: groupOfNames +member: cn=pinny,ou=users,dc=pinniped,dc=dev + +# walruses, groups, pinniped.dev (group of users) +dn: cn=walruses,ou=groups,dc=pinniped,dc=dev +cn: walruses +objectClass: groupOfNames +member: cn=wally,ou=users,dc=pinniped,dc=dev + +# pinnipeds, users, pinniped.dev (group of groups) +dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev +cn: pinnipeds +objectClass: groupOfNames +member: cn=seals,ou=groups,dc=pinniped,dc=dev +member: cn=walruses,ou=groups,dc=pinniped,dc=dev + +# mammals, groups, pinniped.dev (group of both groups and users) +dn: cn=mammals,ou=groups,dc=pinniped,dc=dev +cn: mammals +objectClass: groupOfNames +member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev +member: cn=olive,ou=users,dc=pinniped,dc=dev +` From 51263a0f07c8cc222ec6493f76f9827cfdc688eb Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 13 Apr 2021 16:22:13 -0700 Subject: [PATCH 16/59] Return unauthenticated instead of error for bad username or password - Bad usernames and passwords aren't really errors, since they are based on end-user input. - Other kinds of authentication failures are caused by bad configuration so still treat those as errors. - Empty usernames and passwords are already prevented by our endpoint handler, but just to be safe make sure they cause errors inside the authenticator too. --- internal/oidc/auth/auth_handler.go | 3 +- internal/upstreamldap/upstreamldap.go | 26 ++++++- internal/upstreamldap/upstreamldap_test.go | 65 +++++++++++++--- test/deploy/tools/ldap.yaml | 2 +- ...ldapsearch_test.go => ldap_client_test.go} | 78 ++++++++++--------- 5 files changed, 123 insertions(+), 51 deletions(-) rename test/integration/{ldapsearch_test.go => ldap_client_test.go} (91%) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 701d64da..434d6ce4 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -95,10 +95,11 @@ func handleAuthRequestForLDAPUpstream( authenticateResponse, authenticated, err := ldapUpstream.AuthenticateUser(r.Context(), username, password) if err != nil { - plog.WarningErr("unexpected error during upstream authentication", err, "upstreamName", ldapUpstream.GetName()) + plog.WarningErr("unexpected error during upstream LDAP authentication", err, "upstreamName", ldapUpstream.GetName()) return httperr.New(http.StatusBadGateway, "unexpected error during upstream authentication") } if !authenticated { + plog.Debug("failed upstream LDAP authentication", "upstreamName", ldapUpstream.GetName()) // Return an error according to OIDC spec 3.1.2.6 (second paragraph). err = errors.WithStack(fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider.")) oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index a5af4fe6..cf3b5ffa 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -15,12 +15,15 @@ import ( "github.com/go-ldap/ldap/v3" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" + + "go.pinniped.dev/internal/plog" ) const ( ldapsScheme = "ldaps" distinguishedNameAttributeName = "dn" userSearchFilterInterpolationLocationMarker = "{}" + invalidCredentialsErrorPrefix = `LDAP Result Code 49 "Invalid Credentials":` ) // Conn abstracts the upstream LDAP communication protocol (mostly for testing). @@ -185,6 +188,11 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri return nil, false, fmt.Errorf(`must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`) } + if len(username) == 0 { + // Empty passwords are already handled by go-ldap. + return nil, false, nil + } + conn, err := p.dial(ctx) if err != nil { return nil, false, fmt.Errorf(`error dialing host "%s": %w`, p.Host, err) @@ -200,6 +208,10 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri if err != nil { return nil, false, err } + if len(mappedUsername) == 0 || len(mappedUID) == 0 { + // Couldn't find the username or couldn't bind using the password. + return nil, false, nil + } response := &authenticator.Response{ User: &user.DefaultInfo{ @@ -216,7 +228,12 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, password string if err != nil { return "", "", fmt.Errorf(`error searching for user "%s": %w`, username, err) } - if len(searchResult.Entries) != 1 { + if len(searchResult.Entries) == 0 { + plog.Debug("error finding user: user not found (if this username is valid, please check the user search configuration)", + "upstreamName", p.GetName(), "username", username) + return "", "", nil + } + if len(searchResult.Entries) > 1 { return "", "", fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`, username, len(searchResult.Entries), ) @@ -236,9 +253,14 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, password string return "", "", err } - // Take care that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername! + // Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername! err = conn.Bind(userEntry.DN, password) if err != nil { + plog.DebugErr("error binding for user (if this is not the expected dn for this username, please check the user search configuration)", + err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN) + if strings.HasPrefix(err.Error(), invalidCredentialsErrorPrefix) { + return "", "", nil + } return "", "", fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) } diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 39b19093..c58505dd 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -80,15 +80,16 @@ func TestAuthenticateUser(t *testing.T) { } tests := []struct { - name string - username string - password string - provider *Provider - setupMocks func(conn *mockldapconn.MockConn) - dialError error - wantError string - wantToSkipDial bool - wantAuthResponse *authenticator.Response + name string + username string + password string + provider *Provider + setupMocks func(conn *mockldapconn.MockConn) + dialError error + wantError string + wantToSkipDial bool + wantAuthResponse *authenticator.Response + wantUnauthenticated bool }{ { name: "happy path", @@ -282,6 +283,8 @@ func TestAuthenticateUser(t *testing.T) { }, { name: "when dial fails", + username: testUpstreamUsername, + password: testUpstreamPassword, provider: provider(nil), dialError: errors.New("some dial error"), wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost), @@ -299,6 +302,8 @@ func TestAuthenticateUser(t *testing.T) { }, { name: "when binding as the bind user returns an error", + username: testUpstreamUsername, + password: testUpstreamPassword, provider: provider(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1) @@ -330,7 +335,7 @@ func TestAuthenticateUser(t *testing.T) { }, nil).Times(1) conn.EXPECT().Close().Times(1) }, - wantError: fmt.Sprintf(`searching for user "%s" resulted in 0 search results, but expected 1 result`, testUpstreamUsername), + wantUnauthenticated: true, }, { name: "when searching for the user returns multiple results", @@ -524,6 +529,37 @@ func TestAuthenticateUser(t *testing.T) { }, wantError: fmt.Sprintf(`error binding for user "%s" using provided password against DN "%s": some bind error`, testUpstreamUsername, testSearchResultDNValue), }, + { + name: "when binding as the found user returns a specific invalid credentials error", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(errors.New(`LDAP Result Code 49 "Invalid Credentials": some bind error`)).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantUnauthenticated: true, + }, + { + name: "when no username is specified", + username: "", + password: testUpstreamPassword, + provider: provider(nil), + wantToSkipDial: true, + wantUnauthenticated: true, + }, } for _, test := range tests { @@ -551,11 +587,16 @@ func TestAuthenticateUser(t *testing.T) { require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) - if tt.wantError != "" { + switch { + case tt.wantError != "": require.EqualError(t, err, tt.wantError) require.False(t, authenticated) require.Nil(t, authResponse) - } else { + case tt.wantUnauthenticated: + require.NoError(t, err) + require.False(t, authenticated) + require.Nil(t, authResponse) + default: require.NoError(t, err) require.True(t, authenticated) require.Equal(t, tt.wantAuthResponse, authResponse) diff --git a/test/deploy/tools/ldap.yaml b/test/deploy/tools/ldap.yaml index 20040599..b97151a3 100644 --- a/test/deploy/tools/ldap.yaml +++ b/test/deploy/tools/ldap.yaml @@ -14,7 +14,7 @@ stringData: #@yaml/text-templated-strings ldap.ldif: | # ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! *** - # Here's a good explaination of LDIF: + # Here's a good explanation of LDIF: # https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system # pinniped.dev (organization, root) diff --git a/test/integration/ldapsearch_test.go b/test/integration/ldap_client_test.go similarity index 91% rename from test/integration/ldapsearch_test.go rename to test/integration/ldap_client_test.go index 6bd36241..14524ea3 100644 --- a/test/integration/ldapsearch_test.go +++ b/test/integration/ldap_client_test.go @@ -24,6 +24,8 @@ import ( "go.pinniped.dev/internal/upstreamldap" ) +// Unlike most other integration tests, you can run this test with no special setup, as long as you have Docker. +// It does not depend on Kubernetes. func TestLDAPSearch(t *testing.T) { ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(func() { @@ -57,12 +59,13 @@ func TestLDAPSearch(t *testing.T) { wallyPassword := "password456" // from the LDIF file below tests := []struct { - name string - username string - password string - provider *upstreamldap.Provider - wantError string - wantAuthResponse *authenticator.Response + name string + username string + password string + provider *upstreamldap.Provider + wantError string + wantAuthResponse *authenticator.Response + wantUnauthenticated bool }{ { name: "happy path", @@ -193,18 +196,18 @@ func TestLDAPSearch(t *testing.T) { wantError: `error binding as "cn=admin,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `, }, { - name: "when the end user password is wrong", - username: "pinny", - password: "wrong-pinny-password", - provider: provider(nil), - wantError: `error binding for user "pinny" using provided password against DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 49 "Invalid Credentials": `, + name: "when the end user password is wrong", + username: "pinny", + password: "wrong-pinny-password", + provider: provider(nil), + wantUnauthenticated: true, }, { - name: "when the end user username is wrong", - username: "wrong-username", - password: pinnyPassword, - provider: provider(nil), - wantError: `searching for user "wrong-username" resulted in 0 search results, but expected 1 result`, + name: "when the end user username is wrong", + username: "wrong-username", + password: pinnyPassword, + provider: provider(nil), + wantUnauthenticated: true, }, { name: "when the user search filter does not compile", @@ -329,18 +332,18 @@ func TestLDAPSearch(t *testing.T) { wantError: `error searching for user "pinny": LDAP Result Code 32 "No Such Object": `, }, { - name: "when the search base causes no search results", - username: "pinny", - password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" }), - wantError: `searching for user "pinny" resulted in 0 search results, but expected 1 result`, + name: "when the search base causes no search results", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" }), + wantUnauthenticated: true, }, { - name: "when there is no username specified", - username: "", - password: pinnyPassword, - provider: provider(nil), - wantError: `searching for user "" resulted in 0 search results, but expected 1 result`, + name: "when there is no username specified", + username: "", + password: pinnyPassword, + provider: provider(nil), + wantUnauthenticated: true, }, { name: "when there is no password specified", @@ -350,11 +353,11 @@ func TestLDAPSearch(t *testing.T) { wantError: `error binding for user "pinny" using provided password against DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 206 "Empty password not allowed by the client": ldap: empty password not allowed by the client`, }, { - name: "when the user has no password in their entry", - username: "olive", - password: "anything", - provider: provider(nil), - wantError: `error binding for user "olive" using provided password against DN "cn=olive,ou=users,dc=pinniped,dc=dev": LDAP Result Code 49 "Invalid Credentials": `, + name: "when the user has no password in their entry", + username: "olive", + password: "anything", + provider: provider(nil), + wantUnauthenticated: true, }, } @@ -363,11 +366,16 @@ func TestLDAPSearch(t *testing.T) { t.Run(tt.name, func(t *testing.T) { authResponse, authenticated, err := tt.provider.AuthenticateUser(ctx, tt.username, tt.password) - if tt.wantError != "" { + switch { + case tt.wantError != "": require.EqualError(t, err, tt.wantError) require.False(t, authenticated) require.Nil(t, authResponse) - } else { + case tt.wantUnauthenticated: + require.NoError(t, err) + require.False(t, authenticated) + require.Nil(t, authResponse) + default: require.NoError(t, err) require.True(t, authenticated) require.Equal(t, tt.wantAuthResponse, authResponse) @@ -486,7 +494,7 @@ func writeToNewTempFile(t *testing.T, dir string, filename string, contents []by filePath := path.Join(dir, filename) - err := ioutil.WriteFile(filePath, contents, 0644) + err := ioutil.WriteFile(filePath, contents, 0600) require.NoError(t, err) t.Cleanup(func() { @@ -497,7 +505,7 @@ func writeToNewTempFile(t *testing.T, dir string, filename string, contents []by var testLDIF = ` # ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! *** -# Here's a good explaination of LDIF: +# Here's a good explanation of LDIF: # https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system # pinniped.dev (organization, root) From 14ff5ee4ff45f8728bffc2087fb66e4184f6ad14 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 13 Apr 2021 17:16:57 -0700 Subject: [PATCH 17/59] ldap_upstream_watcher.go: decode and validate CertificateAuthorityData --- .../upstreamwatcher/ldap_upstream_watcher.go | 58 ++++- .../ldap_upstream_watcher_test.go | 245 +++++++++++++++++- internal/upstreamldap/upstreamldap.go | 2 +- internal/upstreamldap/upstreamldap_test.go | 2 + 4 files changed, 295 insertions(+), 12 deletions(-) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index ae214c23..1466ffd3 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -5,6 +5,8 @@ package upstreamwatcher import ( "context" + "crypto/x509" + "encoding/base64" "fmt" corev1 "k8s.io/api/core/v1" @@ -28,7 +30,10 @@ const ( ldapBindAccountSecretType = corev1.SecretTypeBasicAuth // Constants related to conditions. - typeBindSecretValid = "BindSecretValid" + typeBindSecretValid = "BindSecretValid" + tlsConfigurationValid = "TLSConfigurationValid" + noTLSConfigurationMessage = "no TLS configuration provided" + loadedTLSConfigurationMessage = "loaded TLS configuration" ) // UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. @@ -101,10 +106,10 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI { spec := upstream.Spec + result := &upstreamldap.Provider{ - Name: upstream.Name, - Host: spec.Host, - CABundle: []byte(spec.TLS.CertificateAuthorityData), + Name: upstream.Name, + Host: spec.Host, UserSearch: &upstreamldap.UserSearch{ Base: spec.UserSearch.Base, Filter: spec.UserSearch.Filter, @@ -115,6 +120,7 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * } conditions := []*v1alpha1.Condition{ c.validateSecret(upstream, result), + c.validateTLSConfig(upstream, result), } hadErrorCondition := c.updateStatus(ctx, upstream, conditions) if hadErrorCondition { @@ -123,7 +129,49 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * return result } -func (c ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.Provider) *v1alpha1.Condition { +func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.Provider) *v1alpha1.Condition { + tlsSpec := upstream.Spec.TLS + if tlsSpec == nil { + return c.validTLSCondition(noTLSConfigurationMessage) + } + if len(tlsSpec.CertificateAuthorityData) == 0 { + return c.validTLSCondition(loadedTLSConfigurationMessage) + } + + bundle, err := base64.StdEncoding.DecodeString(tlsSpec.CertificateAuthorityData) + if err != nil { + return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", err.Error())) + } + + ca := x509.NewCertPool() + ok := ca.AppendCertsFromPEM(bundle) + if !ok { + return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", errNoCertificates)) + } + + result.CABundle = bundle + return c.validTLSCondition(loadedTLSConfigurationMessage) +} + +func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Condition { + return &v1alpha1.Condition{ + Type: tlsConfigurationValid, + Status: v1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: message, + } +} + +func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Condition { + return &v1alpha1.Condition{ + Type: tlsConfigurationValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonInvalidTLSConfig, + Message: message, + } +} + +func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.Provider) *v1alpha1.Condition { secretName := upstream.Spec.Bind.SecretName secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index 6f836fe6..571d3bd9 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -5,6 +5,7 @@ package upstreamwatcher import ( "context" + "encoding/base64" "fmt" "sort" "testing" @@ -20,6 +21,7 @@ import ( "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" + "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/testutil" @@ -150,15 +152,18 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { testBindUsername = "test-bind-username" testBindPassword = "test-bind-password" testHost = "ldap.example.com:123" - testCABundle = "test-ca-bundle" testUserSearchBase = "test-user-search-base" testUserSearchFilter = "test-user-search-filter" testUsernameAttrName = "test-username-attr" testUIDAttrName = "test-uid-attr" ) - var ( - testValidSecretData = map[string][]byte{"username": []byte(testBindUsername), "password": []byte(testBindPassword)} - ) + + testValidSecretData := map[string][]byte{"username": []byte(testBindUsername), "password": []byte(testBindPassword)} + + testCA, err := certauthority.New("test CA", time.Minute) + require.NoError(t, err) + testCABundle := testCA.Bundle() + testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle) successfulDialer := &comparableDialer{ f: func(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) { @@ -171,7 +176,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, Spec: v1alpha1.LDAPIdentityProviderSpec{ Host: testHost, - TLS: &v1alpha1.LDAPIdentityProviderTLSSpec{CertificateAuthorityData: testCABundle}, + TLS: &v1alpha1.LDAPIdentityProviderTLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, Bind: v1alpha1.LDAPIdentityProviderBindSpec{SecretName: testSecretName}, UserSearch: v1alpha1.LDAPIdentityProviderUserSearchSpec{ Base: testUserSearchBase, @@ -192,7 +197,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { providerForValidUpstream := &upstreamldap.Provider{ Name: testName, Host: testHost, - CABundle: []byte(testCABundle), + CABundle: testCABundle, BindUsername: testBindUsername, BindPassword: testBindPassword, UserSearch: &upstreamldap.UserSearch{ @@ -239,6 +244,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: "loaded bind secret", ObservedGeneration: 1234, }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: 1234, + }, }, }, }}, @@ -263,6 +276,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`secret "%s" not found`, testSecretName), ObservedGeneration: 1234, }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: 1234, + }, }, }, }}, @@ -291,6 +312,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`referenced Secret "%s" has wrong type "some-other-type" (should be "kubernetes.io/basic-auth")`, testSecretName), ObservedGeneration: 1234, }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: 1234, + }, }, }, }}, @@ -318,6 +347,194 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`referenced Secret "%s" is missing required keys ["username" "password"]`, testSecretName), ObservedGeneration: 1234, }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, + { + name: "CertificateAuthorityData is not base64 encoded", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Spec.TLS.CertificateAuthorityData = "this-is-not-base64-encoded" + })}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.Provider{}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: 1234, + }, + { + Type: "TLSConfigurationValid", + Status: "False", + LastTransitionTime: now, + Reason: "InvalidTLSConfig", + Message: "certificateAuthorityData is invalid: illegal base64 data at input byte 4", + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, + { + name: "CertificateAuthorityData is not valid pem data", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Spec.TLS.CertificateAuthorityData = base64.StdEncoding.EncodeToString([]byte("this is not pem data")) + })}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.Provider{}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: 1234, + }, + { + Type: "TLSConfigurationValid", + Status: "False", + LastTransitionTime: now, + Reason: "InvalidTLSConfig", + Message: "certificateAuthorityData is invalid: no certificates found", + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, + { + name: "nil TLS configuration", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Spec.TLS = nil + })}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + wantResultingCache: []*upstreamldap.Provider{ + { + Name: testName, + Host: testHost, + CABundle: nil, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: &upstreamldap.UserSearch{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through + }, + }, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: 1234, + }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "no TLS configuration provided", + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, + { + name: "non-nil TLS configuration with empty CertificateAuthorityData", + ldapDialer: successfulDialer, + inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Spec.TLS.CertificateAuthorityData = "" + })}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + wantResultingCache: []*upstreamldap.Provider{ + { + Name: testName, + Host: testHost, + CABundle: nil, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: &upstreamldap.UserSearch{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through + }, + }, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: 1234, + }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: 1234, + }, }, }, }}, @@ -351,6 +568,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`secret "%s" not found`, "non-existent-secret"), ObservedGeneration: 42, }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: 42, + }, }, }, }, @@ -367,6 +592,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: "loaded bind secret", ObservedGeneration: 1234, }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: 1234, + }, }, }, }, diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index cf3b5ffa..765b5fa9 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -60,7 +60,7 @@ type Provider struct { // the default LDAP port will be used. Host string - // PEM-encoded CA cert bundle to trust when connecting to the LDAP server. + // PEM-encoded CA cert bundle to trust when connecting to the LDAP server. Can be nil. CABundle []byte // BindUsername is the username to use when performing a bind with the upstream LDAP IDP. diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index c58505dd..2615f084 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -45,7 +45,9 @@ var ( func TestAuthenticateUser(t *testing.T) { provider := func(editFunc func(p *Provider)) *Provider { provider := &Provider{ + 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 BindUsername: testBindUsername, BindPassword: testBindPassword, UserSearch: &UserSearch{ From 6bba529b1079e23e5a059bfe8806bb5adfdf0793 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 13 Apr 2021 17:26:53 -0700 Subject: [PATCH 18/59] RBAC rules for ldapidentityproviders to grant permissions to controller --- deploy/supervisor/rbac.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/deploy/supervisor/rbac.yaml b/deploy/supervisor/rbac.yaml index cb84f342..60447f7c 100644 --- a/deploy/supervisor/rbac.yaml +++ b/deploy/supervisor/rbac.yaml @@ -32,6 +32,14 @@ rules: - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") resources: [oidcidentityproviders/status] verbs: [get, patch, update] + - apiGroups: + - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + resources: [ldapidentityproviders] + verbs: [get, list, watch] + - apiGroups: + - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + resources: [ldapidentityproviders/status] + verbs: [get, patch, update] #! We want to be able to read pods/replicasets/deployment so we can learn who our deployment is to set #! as an owner reference. - apiGroups: [""] From 47b66ceaa7bbc9e978882c8340edde58bdb466f2 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 13 Apr 2021 18:11:16 -0700 Subject: [PATCH 19/59] =?UTF-8?q?Passing=20integration=20test=20for=20LDAP?= =?UTF-8?q?=20login!=20=F0=9F=9A=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/deploy/tools/ldap.yaml | 3 +- test/integration/supervisor_login_test.go | 58 ++++++++++++++++------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/test/deploy/tools/ldap.yaml b/test/deploy/tools/ldap.yaml index b97151a3..9bd49df0 100644 --- a/test/deploy/tools/ldap.yaml +++ b/test/deploy/tools/ldap.yaml @@ -2,7 +2,6 @@ #! SPDX-License-Identifier: Apache-2.0 #@ load("@ytt:data", "data") -#@ load("@ytt:base64", "base64") --- apiVersion: v1 kind: Secret @@ -48,7 +47,7 @@ stringData: sn: Seal givenName: Pinny mail: pinny.ldap@example.com - userPassword:: (@= base64.encode(data.values.pinny_ldap_password) @) + userPassword: (@= data.values.pinny_ldap_password @) uid: pinny uidNumber: 1000 gidNumber: 1000 diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 93441949..c2b13073 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -80,7 +80,7 @@ func TestSupervisorLogin(t *testing.T) { library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ Host: env.SupervisorUpstreamLDAP.Host, TLS: &idpv1alpha1.LDAPIdentityProviderTLSSpec{ - CertificateAuthorityData: env.SupervisorUpstreamLDAP.CABundle, + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), }, Bind: idpv1alpha1.LDAPIdentityProviderBindSpec{ SecretName: secret.Name, @@ -93,7 +93,7 @@ func TestSupervisorLogin(t *testing.T) { UniqueID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, - }, "") // TODO: this should be idpv1alpha1.LDAPPhaseReady once we have a controller + }, idpv1alpha1.LDAPPhaseReady) }, requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorizationUsingLDAPIdentityProvider(t, @@ -152,6 +152,10 @@ func testSupervisorLogin( Transport: &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: ca.Pool()}, Proxy: func(req *http.Request) (*url.URL, error) { + if strings.HasPrefix(req.URL.Host, "127.0.0.1") { + // don't proxy requests to localhost to avoid proxying calls to our local callback listener + return nil, nil + } if env.Proxy == "" { t.Logf("passing request for %s with no proxy", req.URL) return nil, nil @@ -249,14 +253,6 @@ func testSupervisorLogin( pkceParam.Method(), ) - // Make the authorize request once "manually" so we can check its response security headers. - authorizeRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) - require.NoError(t, err) - authorizeResp, err := httpClient.Do(authorizeRequest) - require.NoError(t, err) - require.NoError(t, authorizeResp.Body.Close()) - expectSecurityHeaders(t, authorizeResp) - // Perform parameterized auth code acquisition. requestAuthorization(t, downstreamAuthorizeURL, localCallbackServer.URL, httpClient) @@ -350,10 +346,21 @@ func verifyTokenResponse( require.NotEmpty(t, tokenResponse.RefreshToken) } -func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, _ *http.Client) { +func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client) { t.Helper() env := library.IntegrationEnv(t) + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) + defer cancelFunc() + + // Make the authorize request once "manually" so we can check its response security headers. + authorizeRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) + require.NoError(t, err) + authorizeResp, err := httpClient.Do(authorizeRequest) + require.NoError(t, err) + require.NoError(t, authorizeResp.Body.Close()) + expectSecurityHeaders(t, authorizeResp, false) + // Open the web browser and navigate to the downstream authorize URL. page := browsertest.Open(t) t.Logf("opening browser to downstream authorize URL %s", library.MaskTokens(downstreamAuthorizeURL)) @@ -381,18 +388,29 @@ func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAutho authRequest.Header.Set("X-Pinniped-Upstream-Username", upstreamUsername) authRequest.Header.Set("X-Pinniped-Upstream-Password", upstreamPassword) - // The authorize request is supposed to redirect to this test's callback handler, which in turn is supposed to return 200 OK. authResponse, err := httpClient.Do(authRequest) require.NoError(t, err) responseBody, err := ioutil.ReadAll(authResponse.Body) defer authResponse.Body.Close() require.NoError(t, err) + expectSecurityHeaders(t, authResponse, true) - // TODO remove this skip - _ = responseBody // suppress linter until we remove the below skip - t.Skip("The rest of this test will not work until we implement the corresponding production code.") + // A successful authorize request results in a redirect to our localhost callback listener with an authcode param. + require.Equalf(t, http.StatusFound, authResponse.StatusCode, "response body was: %s", string(responseBody)) + redirectLocation := authResponse.Header.Get("Location") + require.Contains(t, redirectLocation, "127.0.0.1") + require.Contains(t, redirectLocation, "/callback") + require.Contains(t, redirectLocation, "code=") - require.Equalf(t, http.StatusOK, authResponse.StatusCode, "response body was: %s", string(responseBody)) + // Follow the redirect. + callbackRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, redirectLocation, nil) + require.NoError(t, err) + + // Our localhost callback listener should have returned 200 OK. + callbackResponse, err := httpClient.Do(callbackRequest) + require.NoError(t, err) + defer callbackResponse.Body.Close() + require.Equal(t, http.StatusOK, callbackResponse.StatusCode) } func startLocalCallbackServer(t *testing.T) *localCallbackServer { @@ -462,7 +480,7 @@ func doTokenExchange(t *testing.T, config *oauth2.Config, tokenResponse *oauth2. t.Logf("exchanged token claims:\n%s", string(indentedClaims)) } -func expectSecurityHeaders(t *testing.T, response *http.Response) { +func expectSecurityHeaders(t *testing.T, response *http.Response, expectFositeToOverrideSome bool) { h := response.Header assert.Equal(t, "default-src 'none'; frame-ancestors 'none'", h.Get("Content-Security-Policy")) assert.Equal(t, "DENY", h.Get("X-Frame-Options")) @@ -470,7 +488,11 @@ func expectSecurityHeaders(t *testing.T, response *http.Response) { assert.Equal(t, "nosniff", h.Get("X-Content-Type-Options")) assert.Equal(t, "no-referrer", h.Get("Referrer-Policy")) assert.Equal(t, "off", h.Get("X-DNS-Prefetch-Control")) - assert.Equal(t, "no-cache,no-store,max-age=0,must-revalidate", h.Get("Cache-Control")) + if expectFositeToOverrideSome { + assert.Equal(t, "no-store", h.Get("Cache-Control")) + } else { + assert.Equal(t, "no-cache,no-store,max-age=0,must-revalidate", h.Get("Cache-Control")) + } assert.Equal(t, "no-cache", h.Get("Pragma")) assert.Equal(t, "0", h.Get("Expires")) } From a6e1a949d24635e8cac855f1f74acb6dbb231e4c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 14 Apr 2021 08:12:15 -0700 Subject: [PATCH 20/59] ldap_client_test.go: mark as integration test so units skip it --- test/integration/ldap_client_test.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/test/integration/ldap_client_test.go b/test/integration/ldap_client_test.go index 14524ea3..2e3cbc14 100644 --- a/test/integration/ldap_client_test.go +++ b/test/integration/ldap_client_test.go @@ -22,11 +22,14 @@ import ( "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/upstreamldap" + "go.pinniped.dev/test/library" ) -// Unlike most other integration tests, you can run this test with no special setup, as long as you have Docker. -// It does not depend on Kubernetes. func TestLDAPSearch(t *testing.T) { + // Unlike most other integration tests, you can run this test with no special setup, as long + // as you have Docker. It does not depend on Kubernetes. + library.SkipUnlessIntegration(t) + ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(func() { cancelFunc() // this will send SIGKILL to the docker process, just in case @@ -157,11 +160,11 @@ func TestLDAPSearch(t *testing.T) { }, { name: "when the UsernameAttribute is sn", - username: "seAl", // note that this is not case-sensitive! sn=Seal + username: "seAl", // note that this is not case-sensitive! sn=Seal. The server decides which fields are compared case-sensitive. password: pinnyPassword, provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "sn" }), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{}}, // note that the final answer is case-sensitive + User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{}}, // note that the final answer has case preserved from the entry }, }, { @@ -202,6 +205,13 @@ func TestLDAPSearch(t *testing.T) { provider: provider(nil), wantUnauthenticated: true, }, + { + name: "when the end user password has the wrong case (passwords are compared as case-sensitive)", + username: "pinny", + password: strings.ToUpper(pinnyPassword), + provider: provider(nil), + wantUnauthenticated: true, + }, { name: "when the end user username is wrong", username: "wrong-username", From e0fe184c8947fbb53325e54b2d5dd90d59a40993 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 14 Apr 2021 08:35:04 -0700 Subject: [PATCH 21/59] Relax cpu limit on ldap server a little to make it start faster - Allowing it to use more CPU during startup decreases startup time from about 25 seconds (on my laptop) down to about 1 second. --- test/deploy/tools/ldap.yaml | 12 ++++++------ test/deploy/tools/proxy.yaml | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/deploy/tools/ldap.yaml b/test/deploy/tools/ldap.yaml index 9bd49df0..0df509ec 100644 --- a/test/deploy/tools/ldap.yaml +++ b/test/deploy/tools/ldap.yaml @@ -148,18 +148,18 @@ spec: containerPort: 1636 resources: requests: - cpu: "10m" + cpu: "100m" #! one-tenth of one CPU memory: "64Mi" limits: - cpu: "10m" + cpu: "200m" #! slapd needs a reasonable amount of CPU during initial startup or else it is slow to start memory: "64Mi" readinessProbe: tcpSocket: port: ldap - initialDelaySeconds: 25 #! typically takes about 30 seconds to start - timeoutSeconds: 120 - periodSeconds: 5 - failureThreshold: 6 + initialDelaySeconds: 2 + timeoutSeconds: 90 + periodSeconds: 2 + failureThreshold: 9 env: #! Example ldapsearch commands that can be run from within the container based on these env vars. #! These will print the whole LDAP tree starting at our root. diff --git a/test/deploy/tools/proxy.yaml b/test/deploy/tools/proxy.yaml index 3a70b3dd..ae293a8a 100644 --- a/test/deploy/tools/proxy.yaml +++ b/test/deploy/tools/proxy.yaml @@ -32,10 +32,10 @@ spec: containerPort: 3128 resources: requests: - cpu: "10m" + cpu: "100m" #! one-tenth of one CPU memory: "64Mi" limits: - cpu: "10m" + cpu: "100m" #! one-tenth of one CPU memory: "64Mi" volumeMounts: - name: log-dir From 939b6b12cc24bc80455f52b6ea69a4af39e6431d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 14 Apr 2021 17:49:40 -0700 Subject: [PATCH 22/59] ldap_client_test.go: refactor to use the LDAP server on the K8s cluster --- test/deploy/tools/cert-issuer.yaml | 17 +- test/deploy/tools/ldap.yaml | 1 + test/integration/ldap_client_test.go | 312 ++++++++------------------- test/library/env.go | 2 +- 4 files changed, 107 insertions(+), 225 deletions(-) diff --git a/test/deploy/tools/cert-issuer.yaml b/test/deploy/tools/cert-issuer.yaml index c1c39ee8..e044b1e7 100644 --- a/test/deploy/tools/cert-issuer.yaml +++ b/test/deploy/tools/cert-issuer.yaml @@ -76,20 +76,31 @@ spec: /tmp/csr.json \ | cfssljson -bare dex + # Cheat and add 127.0.0.1 as an IP SAN so we can use the ldaps port through port forwarding. echo "generating LDAP server certificate..." cfssl gencert \ -ca ca.pem -ca-key ca-key.pem \ -config /tmp/cfssl-default.json \ -profile www \ -cn "ldap.tools.svc.cluster.local" \ - -hostname "ldap.tools.svc.cluster.local" \ + -hostname "ldap.tools.svc.cluster.local,127.0.0.1" \ /tmp/csr.json \ | cfssljson -bare ldap chmod -R 777 /var/certs + echo echo "generated certificates:" ls -l /var/certs + echo + echo "CA cert..." + cat ca.pem | openssl x509 -text + echo + echo "Dex cert..." + cat dex.pem | openssl x509 -text + echo + echo "LDAP cert..." + cat ldap.pem | openssl x509 -text volumeMounts: - name: certs mountPath: /var/certs @@ -100,8 +111,8 @@ spec: args: - -c - | - kubectl get secrets -n tools certs -o jsonpath='created: {.metadata.creationTimestamp}' || \ - kubectl create secret generic -n tools certs --from-file=/var/certs + kubectl create secret generic -n tools certs --from-file=/var/certs \ + --dry-run=client --output yaml | kubectl apply -f - volumeMounts: - name: certs mountPath: /var/certs diff --git a/test/deploy/tools/ldap.yaml b/test/deploy/tools/ldap.yaml index 0df509ec..e98abccb 100644 --- a/test/deploy/tools/ldap.yaml +++ b/test/deploy/tools/ldap.yaml @@ -64,6 +64,7 @@ stringData: sn: Walrus givenName: Wally mail: wally.ldap@example.com + mail: wally.alternate@example.com uid: wally uidNumber: 1001 gidNumber: 1001 diff --git a/test/integration/ldap_client_test.go b/test/integration/ldap_client_test.go index 2e3cbc14..3a201834 100644 --- a/test/integration/ldap_client_test.go +++ b/test/integration/ldap_client_test.go @@ -7,11 +7,9 @@ import ( "context" "fmt" "io" - "io/ioutil" "net" "os" "os/exec" - "path" "strings" "testing" "time" @@ -20,29 +18,36 @@ import ( "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" - "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/upstreamldap" "go.pinniped.dev/test/library" ) func TestLDAPSearch(t *testing.T) { - // Unlike most other integration tests, you can run this test with no special setup, as long - // as you have Docker. It does not depend on Kubernetes. - library.SkipUnlessIntegration(t) + env := library.IntegrationEnv(t) + + // Note that these tests depend on the values hard-coded in the LDIF file in test/deploy/tools/ldap.yaml. + // It requires the test LDAP server from the tools deployment. + if len(env.ToolsNamespace) == 0 { + t.Skip("Skipping test because it requires the test LDAP server in the tools namespace.") + } ctx, cancelFunc := context.WithCancel(context.Background()) t.Cleanup(func() { - cancelFunc() // this will send SIGKILL to the docker process, just in case + cancelFunc() // this will send SIGKILL to the subprocess, just in case }) - port := localhostPort(t) - caBundle := dockerRunLDAPServer(ctx, t, port) + hostPorts := findRecentlyUnusedLocalhostPorts(t, 2) + ldapHostPort := hostPorts[0] + unusedHostPort := hostPorts[1] + + // Expose the the test LDAP server's TLS port on the localhost. + startKubectlPortForward(ctx, t, ldapHostPort, "ldaps", "ldap", env.ToolsNamespace) provider := func(editFunc func(p *upstreamldap.Provider)) *upstreamldap.Provider { provider := &upstreamldap.Provider{ Name: "test-ldap-provider", - Host: "127.0.0.1:" + port, - CABundle: caBundle, + Host: "127.0.0.1:" + ldapHostPort, + CABundle: []byte(env.SupervisorUpstreamLDAP.CABundle), BindUsername: "cn=admin,dc=pinniped,dc=dev", BindPassword: "password", UserSearch: &upstreamldap.UserSearch{ @@ -58,8 +63,7 @@ func TestLDAPSearch(t *testing.T) { return provider } - pinnyPassword := "password123" // from the LDIF file below - wallyPassword := "password456" // from the LDIF file below + pinnyPassword := env.SupervisorUpstreamLDAP.TestUserPassword tests := []struct { name string @@ -79,15 +83,6 @@ func TestLDAPSearch(t *testing.T) { User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, }, }, - { - name: "happy path as a different user", - username: "wally", - password: wallyPassword, - provider: provider(nil), - wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "wally", UID: "1001", Groups: []string{}}, - }, - }, { name: "using a different user search base", username: "pinny", @@ -239,8 +234,8 @@ func TestLDAPSearch(t *testing.T) { name: "when the server is unreachable", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.Host = "127.0.0.1:27534" }), // hopefully this port is not in use on the host running tests - wantError: `error dialing host "127.0.0.1:27534": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:27534: connect: connection refused`, + provider: provider(func(p *upstreamldap.Provider) { p.Host = "127.0.0.1:" + unusedHostPort }), + wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:%s: connect: connection refused`, unusedHostPort, unusedHostPort), }, { name: "when the server is not parsable", @@ -254,33 +249,33 @@ func TestLDAPSearch(t *testing.T) { username: "pinny", password: pinnyPassword, provider: provider(func(p *upstreamldap.Provider) { p.CABundle = []byte("invalid-pem") }), - wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, port), + wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, ldapHostPort), }, { name: "when the CA bundle does not cause the host to be trusted", username: "pinny", password: pinnyPassword, provider: provider(func(p *upstreamldap.Provider) { p.CABundle = nil }), - wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, port), + wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, ldapHostPort), }, { name: "when the UsernameAttribute attribute has multiple values in the entry", username: "wally.ldap@example.com", - password: wallyPassword, + password: "unused-because-error-is-before-bind", provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "mail" }), wantError: `found 2 values for attribute "mail" while searching for user "wally.ldap@example.com", but expected 1 result`, }, { name: "when the UIDAttribute attribute has multiple values in the entry", username: "wally", - password: wallyPassword, + password: "unused-because-error-is-before-bind", provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "mail" }), wantError: `found 2 values for attribute "mail" while searching for user "wally", but expected 1 result`, }, { name: "when the UsernameAttribute attribute is not found in the entry", username: "wally", - password: wallyPassword, + password: "unused-because-error-is-before-bind", provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "cn={}" p.UserSearch.UsernameAttribute = "attr-does-not-exist" @@ -290,7 +285,7 @@ func TestLDAPSearch(t *testing.T) { { name: "when the UIDAttribute attribute is not found in the entry", username: "wally", - password: wallyPassword, + password: "unused-because-error-is-before-bind", provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "attr-does-not-exist" }), wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`, }, @@ -379,81 +374,73 @@ func TestLDAPSearch(t *testing.T) { switch { case tt.wantError != "": require.EqualError(t, err, tt.wantError) - require.False(t, authenticated) + require.False(t, authenticated, "expected the user not to be authenticated, but they were") require.Nil(t, authResponse) case tt.wantUnauthenticated: require.NoError(t, err) - require.False(t, authenticated) + require.False(t, authenticated, "expected the user not to be authenticated, but they were") require.Nil(t, authResponse) default: require.NoError(t, err) - require.True(t, authenticated) + require.True(t, authenticated, "expected the user to be authenticated, but they were not") require.Equal(t, tt.wantAuthResponse, authResponse) } }) } } -func localhostPort(t *testing.T) string { +func startKubectlPortForward(ctx context.Context, t *testing.T, hostPort, remotePort, serviceName, namespace string) { t.Helper() - - unusedPortGrabbingListener, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - - recentlyClaimedHostAndPort := unusedPortGrabbingListener.Addr().String() - require.NoError(t, unusedPortGrabbingListener.Close()) - - splitHostAndPort := strings.Split(recentlyClaimedHostAndPort, ":") - require.Len(t, splitHostAndPort, 2) - - return splitHostAndPort[1] + startLongRunningCommandAndWaitForInitialOutput(ctx, t, + "kubectl", + []string{ + "port-forward", + fmt.Sprintf("service/%s", serviceName), + fmt.Sprintf("%s:%s", hostPort, remotePort), + "-n", namespace, + }, + "Forwarding from ", + "stdout", + ) } -func dockerRunLDAPServer(ctx context.Context, t *testing.T, hostPort string) []byte { +func findRecentlyUnusedLocalhostPorts(t *testing.T, howManyPorts int) []string { t.Helper() - _, err := exec.LookPath("docker") - require.NoError(t, err) - - ca, err := certauthority.New("Test LDAP CA", time.Hour*24) - require.NoError(t, err) - - certPEM, keyPEM, err := ca.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.1")}, time.Hour*24) - require.NoError(t, err) - - tempDir, err := ioutil.TempDir("", "pinniped-test-*") - require.NoError(t, err) - t.Cleanup(func() { - err := os.Remove(tempDir) + listeners := []net.Listener{} + for i := 0; i < howManyPorts; i++ { + unusedPortGrabbingListener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) - }) - - writeToNewTempFile(t, tempDir, "cert.pem", certPEM) - writeToNewTempFile(t, tempDir, "key.pem", keyPEM) - writeToNewTempFile(t, tempDir, "ca.pem", ca.Bundle()) - writeToNewTempFile(t, tempDir, "test.ldif", []byte(testLDIF)) - - dockerArgs := []string{ - "run", - "-e", "BITNAMI_DEBUG=true", - "-e", "LDAP_ADMIN_USERNAME=admin", - "-e", "LDAP_ADMIN_PASSWORD=password", - "-e", "LDAP_ENABLE_TLS=yes", - "-e", "LDAP_TLS_CERT_FILE=/inputs/cert.pem", - "-e", "LDAP_TLS_KEY_FILE=/inputs/key.pem", - "-e", "LDAP_TLS_CA_FILE=/inputs/ca.pem", - "-e", "LDAP_CUSTOM_LDIF_DIR=/inputs", - "-e", "LDAP_ROOT=dc=pinniped,dc=dev", - "-v", tempDir + ":/inputs", - "-p", hostPort + ":1636", - "-m", "64m", - "--rm", // automatically delete the container when finished - "docker.io/bitnami/openldap", + listeners = append(listeners, unusedPortGrabbingListener) } - t.Log("Starting:", "docker", strings.Join(dockerArgs, " ")) + ports := make([]string, len(listeners)) + for i, listener := range listeners { + splitHostAndPort := strings.Split(listener.Addr().String(), ":") + require.Len(t, splitHostAndPort, 2) + ports[i] = splitHostAndPort[1] + } - cmd := exec.CommandContext(ctx, "docker", dockerArgs...) + for _, listener := range listeners { + require.NoError(t, listener.Close()) + } + + return ports +} + +func startLongRunningCommandAndWaitForInitialOutput( + ctx context.Context, + t *testing.T, + command string, + args []string, + waitForOutputToContain string, + waitForOutputOnFd string, // can be either "stdout" or "stderr" +) { + t.Helper() + + t.Logf("Starting: %s %s", command, strings.Join(args, " ")) + + cmd := exec.CommandContext(ctx, command, args...) var stdoutBuf, stderrBuf syncBuffer cmd.Stdout = &stdoutBuf @@ -461,14 +448,25 @@ func dockerRunLDAPServer(ctx context.Context, t *testing.T, hostPort string) []b cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) - err = cmd.Start() + var watchOn *syncBuffer + switch waitForOutputOnFd { + case "stdout": + watchOn = &stdoutBuf + case "stderr": + watchOn = &stderrBuf + default: + t.Fatalf("oops bad argument") + } + + err := cmd.Start() require.NoError(t, err) t.Cleanup(func() { - // docker requires an interrupt signal to end the container. - // This t.Cleanup is registered after the one that cancels the context, so this one will happen first. + // If the cancellation of ctx was already scheduled in a t.Cleanup, then this + // t.Cleanup is registered after the one, so this one will happen first. + // Cancelling ctx will send SIGKILL, which will act as a backup in case + // the process ignored this SIGINT. err := cmd.Process.Signal(os.Interrupt) require.NoError(t, err) - time.Sleep(time.Second) // give a moment before we move on, because we'll send SIGKILL in a later t.Cleanup }) earlyTerminationCh := make(chan bool, 1) @@ -479,9 +477,8 @@ func dockerRunLDAPServer(ctx context.Context, t *testing.T, hostPort string) []b terminatedEarly := false require.Eventually(t, func() bool { - t.Log("Waiting for slapd to start...") - // This substring is contained in the last line of output before the server starts. - if strings.Contains(stderrBuf.String(), " slapd starting\n") { + t.Logf(`Waiting for %s to emit output: "%s"`, command, waitForOutputToContain) + if strings.Contains(watchOn.String(), waitForOutputToContain) { return true } select { @@ -491,136 +488,9 @@ func dockerRunLDAPServer(ctx context.Context, t *testing.T, hostPort string) []b default: // ignore when this non-blocking read found no message } return false - }, 2*time.Minute, time.Second) + }, 1*time.Minute, 1*time.Second) - require.Falsef(t, terminatedEarly, "docker command ended sooner than expected") + require.Falsef(t, terminatedEarly, "subcommand ended sooner than expected") - t.Log("Detected LDAP server has started successfully") - return ca.Bundle() + t.Logf("Detected that %s has started successfully", command) } - -func writeToNewTempFile(t *testing.T, dir string, filename string, contents []byte) { - t.Helper() - - filePath := path.Join(dir, filename) - - err := ioutil.WriteFile(filePath, contents, 0600) - require.NoError(t, err) - - t.Cleanup(func() { - err := os.Remove(filePath) - require.NoError(t, err) - }) -} - -var testLDIF = ` -# ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! *** -# Here's a good explanation of LDIF: -# https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system - -# pinniped.dev (organization, root) -dn: dc=pinniped,dc=dev -objectClass: dcObject -objectClass: organization -dc: pinniped -o: example - -# users, pinniped.dev (organization unit) -dn: ou=users,dc=pinniped,dc=dev -objectClass: organizationalUnit -ou: users - -# groups, pinniped.dev (organization unit) -dn: ou=groups,dc=pinniped,dc=dev -objectClass: organizationalUnit -ou: groups - -# beach-groups, groups, pinniped.dev (organization unit) -dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev -objectClass: organizationalUnit -ou: beach-groups - -# pinny, users, pinniped.dev (user) -dn: cn=pinny,ou=users,dc=pinniped,dc=dev -objectClass: inetOrgPerson -objectClass: posixAccount -objectClass: shadowAccount -cn: pinny -sn: Seal -givenName: Pinny -mail: pinny.ldap@example.com -userPassword: password123 -uid: pinny -uidNumber: 1000 -gidNumber: 1000 -homeDirectory: /home/pinny -loginShell: /bin/bash -gecos: pinny-the-seal - -# wally, users, pinniped.dev -dn: cn=wally,ou=users,dc=pinniped,dc=dev -objectClass: inetOrgPerson -objectClass: posixAccount -objectClass: shadowAccount -cn: wally -sn: Walrus -givenName: Wally -mail: wally.ldap@example.com -mail: wally.alternate@example.com -userPassword: password456 -uid: wally -uidNumber: 1001 -gidNumber: 1001 -homeDirectory: /home/wally -loginShell: /bin/bash -gecos: wally-the-walrus - -# olive, users, pinniped.dev (user without password) -dn: cn=olive,ou=users,dc=pinniped,dc=dev -objectClass: inetOrgPerson -objectClass: posixAccount -objectClass: shadowAccount -cn: olive -sn: Boston Terrier -givenName: Olive -mail: olive.ldap@example.com -uid: olive -uidNumber: 1002 -gidNumber: 1002 -homeDirectory: /home/olive -loginShell: /bin/bash -gecos: olive-the-dog - -# ball-game-players, beach-groups, groups, pinniped.dev (group of users) -dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev -cn: ball-game-players -objectClass: groupOfNames -member: cn=pinny,ou=users,dc=pinniped,dc=dev -member: cn=olive,ou=users,dc=pinniped,dc=dev - -# seals, groups, pinniped.dev (group of users) -dn: cn=seals,ou=groups,dc=pinniped,dc=dev -cn: seals -objectClass: groupOfNames -member: cn=pinny,ou=users,dc=pinniped,dc=dev - -# walruses, groups, pinniped.dev (group of users) -dn: cn=walruses,ou=groups,dc=pinniped,dc=dev -cn: walruses -objectClass: groupOfNames -member: cn=wally,ou=users,dc=pinniped,dc=dev - -# pinnipeds, users, pinniped.dev (group of groups) -dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev -cn: pinnipeds -objectClass: groupOfNames -member: cn=seals,ou=groups,dc=pinniped,dc=dev -member: cn=walruses,ou=groups,dc=pinniped,dc=dev - -# mammals, groups, pinniped.dev (group of both groups and users) -dn: cn=mammals,ou=groups,dc=pinniped,dc=dev -cn: mammals -objectClass: groupOfNames -member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev -member: cn=olive,ou=users,dc=pinniped,dc=dev -` diff --git a/test/library/env.go b/test/library/env.go index 1e54fbd0..78aeaec5 100644 --- a/test/library/env.go +++ b/test/library/env.go @@ -236,7 +236,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { result.SupervisorUpstreamLDAP = TestLDAPUpstream{ Host: needEnv(t, "PINNIPED_TEST_LDAP_HOST"), - CABundle: needEnv(t, "PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE"), + CABundle: base64Decoded(t, needEnv(t, "PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE")), BindUsername: needEnv(t, "PINNIPED_TEST_LDAP_BIND_ACCOUNT_USERNAME"), BindPassword: needEnv(t, "PINNIPED_TEST_LDAP_BIND_ACCOUNT_PASSWORD"), UserSearchBase: needEnv(t, "PINNIPED_TEST_LDAP_USERS_SEARCH_BASE"), From 12a36363515d7905412e2fe25fb54fbeeb7a06f0 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 14 Apr 2021 20:39:01 -0700 Subject: [PATCH 23/59] base64 once instead of thrice --- hack/prepare-for-integration-tests.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index b697d7ce..f9838223 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -293,7 +293,7 @@ popd >/dev/null # # Download the test CA bundle that was generated in the Dex pod. # -test_ca_bundle_pem="$(kubectl get secrets -n tools certs -o go-template='{{index .data "ca.pem" | base64decode}}')" +test_ca_bundle_pem="$(kubectl get secrets -n tools certs -o go-template='{{index .data "ca.pem" | base64decode}}' | base64)" # # Create the environment file. @@ -323,7 +323,7 @@ export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345" export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344" export PINNIPED_TEST_PROXY=http://127.0.0.1:12346 export PINNIPED_TEST_LDAP_HOST=ldap.tools.svc.cluster.local -export PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE=$(echo "${test_ca_bundle_pem}" | base64 ) +export PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE="${test_ca_bundle_pem}" export PINNIPED_TEST_LDAP_BIND_ACCOUNT_USERNAME="cn=admin,dc=pinniped,dc=dev" export PINNIPED_TEST_LDAP_BIND_ACCOUNT_PASSWORD=password export PINNIPED_TEST_LDAP_USERS_SEARCH_BASE="ou=users,dc=pinniped,dc=dev" @@ -340,13 +340,13 @@ export PINNIPED_TEST_LDAP_EXPECTED_INDIRECT_GROUPS_DN="cn=pinnipeds,ou=groups,dc export PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_CN="ball-game-players;seals" export PINNIPED_TEST_LDAP_EXPECTED_INDIRECT_GROUPS_CN="pinnipeds;mammals" export PINNIPED_TEST_CLI_OIDC_ISSUER=https://dex.tools.svc.cluster.local/dex -export PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE=$(echo "${test_ca_bundle_pem}" | base64 ) +export PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}" export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli export PINNIPED_TEST_CLI_OIDC_CALLBACK_URL=http://127.0.0.1:48095/callback export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com export PINNIPED_TEST_CLI_OIDC_PASSWORD=${dex_test_password} export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER=https://dex.tools.svc.cluster.local/dex -export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE=$(echo "${test_ca_bundle_pem}" | base64 ) +export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}" export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES=email export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM=email export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM=groups From 5c28d36c9ba6fc25d7f96da1da40d70a49e06674 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 15 Apr 2021 07:59:38 -0700 Subject: [PATCH 24/59] Redact some params of URLs in logs to avoid printing sensitive info --- .../concierge_impersonation_proxy_test.go | 6 +++--- test/integration/supervisor_login_test.go | 4 ++-- test/library/iotest.go | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index 372f7ca4..c7937e65 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -745,7 +745,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl dialer.Proxy = func(req *http.Request) (*url.URL, error) { proxyURL, err := url.Parse(env.Proxy) require.NoError(t, err) - t.Logf("passing request for %s through proxy %s", req.URL, proxyURL.String()) + t.Logf("passing request for %s through proxy %s", library.RedactURLParams(req.URL), proxyURL.String()) return proxyURL, nil } } @@ -823,7 +823,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl httpTransport.Proxy = func(req *http.Request) (*url.URL, error) { proxyURL, err := url.Parse(env.Proxy) require.NoError(t, err) - t.Logf("passing request for %s through proxy %s", req.URL, proxyURL.String()) + t.Logf("passing request for %s through proxy %s", library.RedactURLParams(req.URL), proxyURL.String()) return proxyURL, nil } } @@ -1146,7 +1146,7 @@ func kubeconfigProxyFunc(t *testing.T, squidProxyURL string) func(req *http.Requ t.Helper() parsedSquidProxyURL, err := url.Parse(squidProxyURL) require.NoError(t, err) - t.Logf("passing request for %s through proxy %s", req.URL, parsedSquidProxyURL.String()) + t.Logf("passing request for %s through proxy %s", library.RedactURLParams(req.URL), parsedSquidProxyURL.String()) return parsedSquidProxyURL, nil } } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index c2b13073..2ee9f8b4 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -157,12 +157,12 @@ func testSupervisorLogin( return nil, nil } if env.Proxy == "" { - t.Logf("passing request for %s with no proxy", req.URL) + t.Logf("passing request for %s with no proxy", library.RedactURLParams(req.URL)) return nil, nil } proxyURL, err := url.Parse(env.Proxy) require.NoError(t, err) - t.Logf("passing request for %s through proxy %s", req.URL, proxyURL.String()) + t.Logf("passing request for %s through proxy %s", library.RedactURLParams(req.URL), proxyURL.String()) return proxyURL, nil }, }, diff --git a/test/library/iotest.go b/test/library/iotest.go index 7ac175b3..d103550d 100644 --- a/test/library/iotest.go +++ b/test/library/iotest.go @@ -6,6 +6,7 @@ package library import ( "fmt" "io" + "net/url" "regexp" "strings" "testing" @@ -50,3 +51,15 @@ func MaskTokens(in string) string { return fmt.Sprintf("[...%d bytes...]", len(t)) }) } + +// Remove any potentially sensitive query param and fragment values for test logging. +func RedactURLParams(fullURL *url.URL) string { + copyOfURL, _ := url.Parse(fullURL.String()) + if len(copyOfURL.RawQuery) > 0 { + copyOfURL.RawQuery = "redacted" + } + if len(copyOfURL.Fragment) > 0 { + copyOfURL.Fragment = "redacted" + } + return copyOfURL.String() +} From e6e6497022b0661c04b309da03f0bc9234df4ed7 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 15 Apr 2021 10:25:35 -0700 Subject: [PATCH 25/59] Introduce upstreamldap.New to prevent changes to the underlying config Makes it easier to support using the same upstreamldap.Provider from multiple goroutines safely. --- .../upstreamwatcher/ldap_upstream_watcher.go | 14 +- .../ldap_upstream_watcher_test.go | 32 +-- internal/upstreamldap/upstreamldap.go | 68 +++--- internal/upstreamldap/upstreamldap_test.go | 199 ++++++++++-------- test/integration/ldap_client_test.go | 177 ++++++++++------ 5 files changed, 296 insertions(+), 194 deletions(-) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index 1466ffd3..c119ac57 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -107,10 +107,10 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI { spec := upstream.Spec - result := &upstreamldap.Provider{ + config := &upstreamldap.ProviderConfig{ Name: upstream.Name, Host: spec.Host, - UserSearch: &upstreamldap.UserSearch{ + UserSearch: upstreamldap.UserSearchConfig{ Base: spec.UserSearch.Base, Filter: spec.UserSearch.Filter, UsernameAttribute: spec.UserSearch.Attributes.Username, @@ -119,17 +119,17 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * Dialer: c.ldapDialer, } conditions := []*v1alpha1.Condition{ - c.validateSecret(upstream, result), - c.validateTLSConfig(upstream, result), + c.validateSecret(upstream, config), + c.validateTLSConfig(upstream, config), } hadErrorCondition := c.updateStatus(ctx, upstream, conditions) if hadErrorCondition { return nil } - return result + return upstreamldap.New(*config) } -func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.Provider) *v1alpha1.Condition { +func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.ProviderConfig) *v1alpha1.Condition { tlsSpec := upstream.Spec.TLS if tlsSpec == nil { return c.validTLSCondition(noTLSConfigurationMessage) @@ -171,7 +171,7 @@ func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Co } } -func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.Provider) *v1alpha1.Condition { +func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.ProviderConfig) *v1alpha1.Condition { secretName := upstream.Spec.Bind.SecretName secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index 571d3bd9..ca362db9 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -194,13 +194,13 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { return deepCopy } - providerForValidUpstream := &upstreamldap.Provider{ + providerConfigForValidUpstream := &upstreamldap.ProviderConfig{ Name: testName, Host: testHost, CABundle: testCABundle, BindUsername: testBindUsername, BindPassword: testBindPassword, - UserSearch: &upstreamldap.UserSearch{ + UserSearch: upstreamldap.UserSearchConfig{ Base: testUserSearchBase, Filter: testUserSearchFilter, UsernameAttribute: testUsernameAttrName, @@ -215,7 +215,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { inputSecrets []runtime.Object ldapDialer upstreamldap.LDAPDialer wantErr string - wantResultingCache []*upstreamldap.Provider + wantResultingCache []*upstreamldap.ProviderConfig wantResultingUpstreams []v1alpha1.LDAPIdentityProvider }{ { @@ -230,7 +230,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, - wantResultingCache: []*upstreamldap.Provider{providerForValidUpstream}, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ @@ -262,7 +262,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{validUpstream}, inputSecrets: []runtime.Object{}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.Provider{}, + wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ @@ -298,7 +298,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Data: testValidSecretData, }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.Provider{}, + wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ @@ -333,7 +333,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Type: corev1.SecretTypeBasicAuth, }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.Provider{}, + wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ @@ -371,7 +371,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Data: testValidSecretData, }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.Provider{}, + wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ @@ -409,7 +409,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Data: testValidSecretData, }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.Provider{}, + wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ @@ -446,14 +446,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, - wantResultingCache: []*upstreamldap.Provider{ + wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, Host: testHost, CABundle: nil, BindUsername: testBindUsername, BindPassword: testBindPassword, - UserSearch: &upstreamldap.UserSearch{ + UserSearch: upstreamldap.UserSearchConfig{ Base: testUserSearchBase, Filter: testUserSearchFilter, UsernameAttribute: testUsernameAttrName, @@ -498,14 +498,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, - wantResultingCache: []*upstreamldap.Provider{ + wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, Host: testHost, CABundle: nil, BindUsername: testBindUsername, BindPassword: testBindPassword, - UserSearch: &upstreamldap.UserSearch{ + UserSearch: upstreamldap.UserSearchConfig{ Base: testUserSearchBase, Filter: testUserSearchFilter, UsernameAttribute: testUsernameAttrName, @@ -553,7 +553,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Data: testValidSecretData, }}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.Provider{providerForValidUpstream}, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{ { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42}, @@ -616,7 +616,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) cache := provider.NewDynamicUpstreamIDPProvider() cache.SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ - &upstreamldap.Provider{Name: "initial-entry"}, + upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}), }) controller := NewLDAPUpstreamWatcherController( @@ -647,7 +647,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) for i := range actualIDPList { actualIDP := actualIDPList[i].(*upstreamldap.Provider) - require.Equal(t, tt.wantResultingCache[i], actualIDP) + require.Equal(t, *tt.wantResultingCache[i], actualIDP.GetConfig()) } actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 765b5fa9..f27551c7 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -50,9 +50,10 @@ func (f LDAPDialerFunc) Dial(ctx context.Context, hostAndPort string) (Conn, err return f(ctx, hostAndPort) } -// Provider includes all of the settings for connection and searching for users and groups in +// ProviderConfig includes all of the settings for connection and searching for users and groups in // the upstream LDAP IDP. It also provides methods for testing the connection and performing logins. -type Provider struct { +// The nested structs are not pointer fields to enable deep copy on function params and return values. +type ProviderConfig struct { // Name is the unique name of this upstream LDAP IDP. Name string @@ -70,14 +71,14 @@ type Provider struct { BindPassword string // UserSearch contains information about how to search for users in the upstream LDAP IDP. - UserSearch *UserSearch + UserSearch UserSearchConfig // Dialer exists to enable testing. When nil, will use a default appropriate for production use. Dialer LDAPDialer } -// UserSearch contains information about how to search for users in the upstream LDAP IDP. -type UserSearch struct { +// UserSearchConfig contains information about how to search for users in the upstream LDAP IDP. +type UserSearchConfig struct { // Base is the base DN to use for the user search in the upstream LDAP IDP. Base string @@ -93,13 +94,28 @@ type UserSearch struct { UIDAttribute string } +type Provider struct { + c ProviderConfig +} + +// Create a Provider. The config is not a pointer to ensure that a copy of the config is created, +// making the resulting Provider use an effectively read-only configuration. +func New(config ProviderConfig) *Provider { + return &Provider{c: config} +} + +// A reader for the config. Returns a copy of the config to keep the underlying config read-only. +func (p *Provider) GetConfig() ProviderConfig { + return p.c +} + func (p *Provider) dial(ctx context.Context) (Conn, error) { - hostAndPort, err := hostAndPortWithDefaultPort(p.Host, ldap.DefaultLdapsPort) + hostAndPort, err := hostAndPortWithDefaultPort(p.c.Host, ldap.DefaultLdapsPort) if err != nil { return nil, ldap.NewError(ldap.ErrorNetwork, err) } - if p.Dialer != nil { - return p.Dialer.Dial(ctx, hostAndPort) + if p.c.Dialer != nil { + return p.c.Dialer.Dial(ctx, hostAndPort) } return p.dialTLS(ctx, hostAndPort) } @@ -109,8 +125,8 @@ func (p *Provider) dial(ctx context.Context) (Conn, error) { // so we implement it ourselves, heavily inspired by ldap.DialURL. func (p *Provider) dialTLS(ctx context.Context, hostAndPort string) (Conn, error) { rootCAs := x509.NewCertPool() - if p.CABundle != nil { - if !rootCAs.AppendCertsFromPEM(p.CABundle) { + if p.c.CABundle != nil { + if !rootCAs.AppendCertsFromPEM(p.c.CABundle) { return nil, ldap.NewError(ldap.ErrorNetwork, fmt.Errorf("could not parse CA bundle")) } } @@ -154,14 +170,14 @@ func hostAndPortWithDefaultPort(hostAndPort string, defaultPort string) (string, // A name for this upstream provider. func (p *Provider) GetName() string { - return p.Name + return p.c.Name } // Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234". // This URL is not used for connecting to the provider, but rather is used for creating a globally unique user // identifier by being combined with the user's UID, since user UIDs are only unique within one provider. func (p *Provider) GetURL() string { - return fmt.Sprintf("%s://%s", ldapsScheme, p.Host) + return fmt.Sprintf("%s://%s", ldapsScheme, p.c.Host) } // TestConnection provides a method for testing the connection and bind settings. It performs a dial and bind @@ -183,7 +199,7 @@ func (p *Provider) TestAuthenticateUser(ctx context.Context, testUsername string // Authenticate a 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) { - if p.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.UserSearch.Filter) == 0 { + if p.c.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.c.UserSearch.Filter) == 0 { // LDAP search filters do not allow searching by DN. return nil, false, fmt.Errorf(`must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`) } @@ -195,13 +211,13 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri conn, err := p.dial(ctx) if err != nil { - return nil, false, fmt.Errorf(`error dialing host "%s": %w`, p.Host, err) + return nil, false, fmt.Errorf(`error dialing host "%s": %w`, p.c.Host, err) } defer conn.Close() - err = conn.Bind(p.BindUsername, p.BindPassword) + err = conn.Bind(p.c.BindUsername, p.c.BindPassword) if err != nil { - return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.BindUsername, err) + return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err) } mappedUsername, mappedUID, err := p.searchAndBindUser(conn, username, password) @@ -243,12 +259,12 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, password string return "", "", fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username) } - mappedUsername, err := p.getSearchResultAttributeValue(p.UserSearch.UsernameAttribute, userEntry, username) + mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username) if err != nil { return "", "", err } - mappedUID, err := p.getSearchResultAttributeValue(p.UserSearch.UIDAttribute, userEntry, username) + mappedUID, err := p.getSearchResultAttributeValue(p.c.UserSearch.UIDAttribute, userEntry, username) if err != nil { return "", "", err } @@ -270,7 +286,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, password string func (p *Provider) userSearchRequest(username string) *ldap.SearchRequest { // See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options. return &ldap.SearchRequest{ - BaseDN: p.UserSearch.Base, + BaseDN: p.c.UserSearch.Base, Scope: ldap.ScopeWholeSubtree, DerefAliases: ldap.DerefAlways, // TODO what's the best value here? SizeLimit: 2, @@ -284,21 +300,21 @@ func (p *Provider) userSearchRequest(username string) *ldap.SearchRequest { func (p *Provider) userSearchRequestedAttributes() []string { attributes := []string{} - if p.UserSearch.UsernameAttribute != distinguishedNameAttributeName { - attributes = append(attributes, p.UserSearch.UsernameAttribute) + if p.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName { + attributes = append(attributes, p.c.UserSearch.UsernameAttribute) } - if p.UserSearch.UIDAttribute != distinguishedNameAttributeName { - attributes = append(attributes, p.UserSearch.UIDAttribute) + if p.c.UserSearch.UIDAttribute != distinguishedNameAttributeName { + attributes = append(attributes, p.c.UserSearch.UIDAttribute) } return attributes } func (p *Provider) userSearchFilter(username string) string { safeUsername := p.escapeUsernameForSearchFilter(username) - if len(p.UserSearch.Filter) == 0 { - return fmt.Sprintf("(%s=%s)", p.UserSearch.UsernameAttribute, safeUsername) + if len(p.c.UserSearch.Filter) == 0 { + return fmt.Sprintf("(%s=%s)", p.c.UserSearch.UsernameAttribute, safeUsername) } - filter := strings.ReplaceAll(p.UserSearch.Filter, userSearchFilterInterpolationLocationMarker, safeUsername) + filter := strings.ReplaceAll(p.c.UserSearch.Filter, userSearchFilterInterpolationLocationMarker, safeUsername) if strings.HasPrefix(filter, "(") && strings.HasSuffix(filter, ")") { return filter } diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 2615f084..b6b21846 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -43,14 +43,14 @@ var ( ) func TestAuthenticateUser(t *testing.T) { - provider := func(editFunc func(p *Provider)) *Provider { - provider := &Provider{ + providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig { + config := &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 BindUsername: testBindUsername, BindPassword: testBindPassword, - UserSearch: &UserSearch{ + UserSearch: UserSearchConfig{ Base: testUserSearchBase, Filter: testUserSearchFilter, UsernameAttribute: testUserSearchUsernameAttribute, @@ -58,9 +58,9 @@ func TestAuthenticateUser(t *testing.T) { }, } if editFunc != nil { - editFunc(provider) + editFunc(config) } - return provider + return config } expectedSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { @@ -85,7 +85,7 @@ func TestAuthenticateUser(t *testing.T) { name string username string password string - provider *Provider + providerConfig *ProviderConfig setupMocks func(conn *mockldapconn.MockConn) dialError error wantError string @@ -94,10 +94,10 @@ func TestAuthenticateUser(t *testing.T) { wantUnauthenticated bool }{ { - name: "happy path", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "happy path", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -128,7 +128,7 @@ func TestAuthenticateUser(t *testing.T) { name: "when the user search filter is already wrapped by parenthesis then it is not wrapped again", username: testUpstreamUsername, password: testUpstreamPassword, - provider: provider(func(p *Provider) { + providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.Filter = "(" + testUserSearchFilter + ")" }), setupMocks: func(conn *mockldapconn.MockConn) { @@ -159,7 +159,7 @@ func TestAuthenticateUser(t *testing.T) { name: "when the UsernameAttribute is dn and there is a user search filter provided", username: testUpstreamUsername, password: testUpstreamPassword, - provider: provider(func(p *Provider) { + providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.UsernameAttribute = "dn" }), setupMocks: func(conn *mockldapconn.MockConn) { @@ -191,7 +191,7 @@ func TestAuthenticateUser(t *testing.T) { name: "when the UIDAttribute is dn", username: testUpstreamUsername, password: testUpstreamPassword, - provider: provider(func(p *Provider) { + providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.UIDAttribute = "dn" }), setupMocks: func(conn *mockldapconn.MockConn) { @@ -223,7 +223,7 @@ func TestAuthenticateUser(t *testing.T) { name: "when Filter is blank it derives a search filter from the UsernameAttribute", username: testUpstreamUsername, password: testUpstreamPassword, - provider: provider(func(p *Provider) { + providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.Filter = "" }), setupMocks: func(conn *mockldapconn.MockConn) { @@ -253,10 +253,10 @@ func TestAuthenticateUser(t *testing.T) { }, }, { - name: "when the username has special LDAP search filter characters then they must be properly escaped in the search filter", - username: `a&b|c(d)e\f*g`, - password: testUpstreamPassword, - provider: provider(nil), + name: "when the username has special LDAP search filter characters then they must be properly escaped in the search filter", + username: `a&b|c(d)e\f*g`, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { @@ -284,18 +284,18 @@ func TestAuthenticateUser(t *testing.T) { }, }, { - name: "when dial fails", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), - dialError: errors.New("some dial error"), - wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost), + name: "when dial fails", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + dialError: errors.New("some dial error"), + wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost), }, { name: "when the UsernameAttribute is dn and there is not a user search filter provided", username: testUpstreamUsername, password: testUpstreamPassword, - provider: provider(func(p *Provider) { + providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.UsernameAttribute = "dn" p.UserSearch.Filter = "" }), @@ -303,10 +303,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`, }, { - name: "when binding as the bind user returns an error", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when binding as the bind user returns an error", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1) conn.EXPECT().Close().Times(1) @@ -314,10 +314,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`error binding as "%s" before user search: some bind error`, testBindUsername), }, { - name: "when searching for the user returns an error", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns an error", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(nil, errors.New("some search error")).Times(1) @@ -326,10 +326,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`error searching for user "%s": some search error`, testUpstreamUsername), }, { - name: "when searching for the user returns no results", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns no results", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -340,10 +340,10 @@ func TestAuthenticateUser(t *testing.T) { wantUnauthenticated: true, }, { - name: "when searching for the user returns multiple results", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns multiple results", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -357,10 +357,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`searching for user "%s" resulted in 2 search results, but expected 1 result`, testUpstreamUsername), }, { - name: "when searching for the user returns a user without a DN", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns a user without a DN", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -373,10 +373,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`searching for user "%s" resulted in search result without DN`, testUpstreamUsername), }, { - name: "when searching for the user returns a user without an expected username attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns a user without an expected username attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -394,10 +394,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`found 0 values for attribute "%s" while searching for user "%s", but expected 1 result`, testUserSearchUsernameAttribute, testUpstreamUsername), }, { - name: "when searching for the user returns a user with too many values for the expected username attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns a user with too many values for the expected username attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -419,10 +419,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`found 2 values for attribute "%s" while searching for user "%s", but expected 1 result`, testUserSearchUsernameAttribute, testUpstreamUsername), }, { - name: "when searching for the user returns a user with an empty value for the expected username attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns a user with an empty value for the expected username attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -441,10 +441,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, testUserSearchUsernameAttribute, testUpstreamUsername), }, { - name: "when searching for the user returns a user without an expected UID attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns a user without an expected UID attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -462,10 +462,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`found 0 values for attribute "%s" while searching for user "%s", but expected 1 result`, testUserSearchUIDAttribute, testUpstreamUsername), }, { - name: "when searching for the user returns a user with too many values for the expected UID attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns a user with too many values for the expected UID attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -487,10 +487,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`found 2 values for attribute "%s" while searching for user "%s", but expected 1 result`, testUserSearchUIDAttribute, testUpstreamUsername), }, { - name: "when searching for the user returns a user with an empty value for the expected UID attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when searching for the user returns a user with an empty value for the expected UID attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -509,10 +509,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, testUserSearchUIDAttribute, testUpstreamUsername), }, { - name: "when binding as the found user returns an error", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when binding as the found user returns an error", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -532,10 +532,10 @@ func TestAuthenticateUser(t *testing.T) { wantError: fmt.Sprintf(`error binding for user "%s" using provided password against DN "%s": some bind error`, testUpstreamUsername, testSearchResultDNValue), }, { - name: "when binding as the found user returns a specific invalid credentials error", - username: testUpstreamUsername, - password: testUpstreamPassword, - provider: provider(nil), + name: "when binding as the found user returns a specific invalid credentials error", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ @@ -558,7 +558,7 @@ func TestAuthenticateUser(t *testing.T) { name: "when no username is specified", username: "", password: testUpstreamPassword, - provider: provider(nil), + providerConfig: providerConfig(nil), wantToSkipDial: true, wantUnauthenticated: true, }, @@ -576,16 +576,17 @@ func TestAuthenticateUser(t *testing.T) { } dialWasAttempted := false - tt.provider.Dialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) { + tt.providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) { dialWasAttempted = true - require.Equal(t, tt.provider.Host, hostAndPort) + require.Equal(t, tt.providerConfig.Host, hostAndPort) if tt.dialError != nil { return nil, tt.dialError } return conn, nil }) - authResponse, authenticated, err := tt.provider.AuthenticateUser(context.Background(), tt.username, tt.password) + provider := New(*tt.providerConfig) + authResponse, authenticated, err := provider.AuthenticateUser(context.Background(), tt.username, tt.password) require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) @@ -607,9 +608,37 @@ func TestAuthenticateUser(t *testing.T) { } } +func TestGetConfig(t *testing.T) { + c := ProviderConfig{ + Name: "original-provider-name", + Host: testHost, + CABundle: []byte("some-ca-bundle"), + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUserSearchUsernameAttribute, + UIDAttribute: testUserSearchUIDAttribute, + }, + } + p := New(c) + require.Equal(t, c, p.c) + require.Equal(t, c, p.GetConfig()) + + // The original config can be changed without impacting the provider, since the provider made a copy of the config. + c.Name = "changed-name" + require.Equal(t, "original-provider-name", p.c.Name) + + // The return value of GetConfig can be modified without impacting the provider, since it is a copy of the config. + returnedConfig := p.GetConfig() + returnedConfig.Name = "changed-name" + require.Equal(t, "original-provider-name", p.c.Name) +} + func TestGetURL(t *testing.T) { - require.Equal(t, "ldaps://ldap.example.com:1234", (&Provider{Host: "ldap.example.com:1234"}).GetURL()) - require.Equal(t, "ldaps://ldap.example.com", (&Provider{Host: "ldap.example.com"}).GetURL()) + require.Equal(t, "ldaps://ldap.example.com:1234", New(ProviderConfig{Host: "ldap.example.com:1234"}).GetURL()) + require.Equal(t, "ldaps://ldap.example.com", New(ProviderConfig{Host: "ldap.example.com"}).GetURL()) } // Testing of host parsing, TLS negotiation, and CA bundle, etc. for the production code's dialer. @@ -673,11 +702,11 @@ func TestRealTLSDialing(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - provider := &Provider{ + provider := New(ProviderConfig{ Host: test.host, CABundle: test.caBundle, Dialer: nil, // this test is for the default (production) dialer - } + }) conn, err := provider.dial(test.context) if conn != nil { defer conn.Close() diff --git a/test/integration/ldap_client_test.go b/test/integration/ldap_client_test.go index 3a201834..fb49a099 100644 --- a/test/integration/ldap_client_test.go +++ b/test/integration/ldap_client_test.go @@ -43,24 +43,12 @@ func TestLDAPSearch(t *testing.T) { // Expose the the test LDAP server's TLS port on the localhost. startKubectlPortForward(ctx, t, ldapHostPort, "ldaps", "ldap", env.ToolsNamespace) - provider := func(editFunc func(p *upstreamldap.Provider)) *upstreamldap.Provider { - provider := &upstreamldap.Provider{ - Name: "test-ldap-provider", - Host: "127.0.0.1:" + ldapHostPort, - CABundle: []byte(env.SupervisorUpstreamLDAP.CABundle), - BindUsername: "cn=admin,dc=pinniped,dc=dev", - BindPassword: "password", - UserSearch: &upstreamldap.UserSearch{ - Base: "ou=users,dc=pinniped,dc=dev", - Filter: "", // defaults to UsernameAttribute={}, i.e. "cn={}" in this case - UsernameAttribute: "cn", - UIDAttribute: "uidNumber", - }, - } + providerConfig := func(editFunc func(p *upstreamldap.ProviderConfig)) *upstreamldap.ProviderConfig { + providerConfig := defaultProviderConfig(env, ldapHostPort) if editFunc != nil { - editFunc(provider) + editFunc(providerConfig) } - return provider + return providerConfig } pinnyPassword := env.SupervisorUpstreamLDAP.TestUserPassword @@ -78,7 +66,7 @@ func TestLDAPSearch(t *testing.T) { name: "happy path", username: "pinny", password: pinnyPassword, - provider: provider(nil), + provider: upstreamldap.New(*providerConfig(nil)), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, }, @@ -87,7 +75,7 @@ func TestLDAPSearch(t *testing.T) { name: "using a different user search base", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "dc=pinniped,dc=dev" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, }, @@ -96,7 +84,7 @@ func TestLDAPSearch(t *testing.T) { name: "when the user search filter is already wrapped by parenthesis", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "(cn={})" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, }, @@ -105,10 +93,10 @@ func TestLDAPSearch(t *testing.T) { name: "when the UsernameAttribute is dn and a user search filter is provided", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "dn" p.UserSearch.Filter = "cn={}" - }), + })), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{}}, }, @@ -117,9 +105,9 @@ func TestLDAPSearch(t *testing.T) { name: "when the user search filter allows for different ways of logging in and the first one is used", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(|(cn={})(mail={}))" - }), + })), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, }, @@ -128,9 +116,9 @@ func TestLDAPSearch(t *testing.T) { name: "when the user search filter allows for different ways of logging in and the second one is used", username: "pinny.ldap@example.com", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(|(cn={})(mail={}))" - }), + })), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, }, @@ -139,7 +127,7 @@ func TestLDAPSearch(t *testing.T) { name: "when the UIDAttribute is dn", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "dn" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{}}, }, @@ -148,7 +136,7 @@ func TestLDAPSearch(t *testing.T) { name: "when the UIDAttribute is sn", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "sn" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{}}, }, @@ -157,7 +145,7 @@ func TestLDAPSearch(t *testing.T) { name: "when the UsernameAttribute is sn", username: "seAl", // note that this is not case-sensitive! sn=Seal. The server decides which fields are compared case-sensitive. password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "sn" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })), wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{}}, // note that the final answer has case preserved from the entry }, @@ -166,202 +154,202 @@ func TestLDAPSearch(t *testing.T) { name: "when the UsernameAttribute is dn and there is no user search filter provided", username: "cn=pinny,ou=users,dc=pinniped,dc=dev", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "dn" p.UserSearch.Filter = "" - }), + })), wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`, }, { name: "when the bind user username is not a valid DN", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.BindUsername = "invalid-dn" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.BindUsername = "invalid-dn" })), wantError: `error binding as "invalid-dn" before user search: LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, }, { name: "when the bind user username is wrong", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.BindUsername = "cn=wrong,dc=pinniped,dc=dev" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.BindUsername = "cn=wrong,dc=pinniped,dc=dev" })), wantError: `error binding as "cn=wrong,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `, }, { name: "when the bind user password is wrong", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.BindPassword = "wrong-password" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.BindPassword = "wrong-password" })), wantError: `error binding as "cn=admin,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `, }, { name: "when the end user password is wrong", username: "pinny", password: "wrong-pinny-password", - provider: provider(nil), + provider: upstreamldap.New(*providerConfig(nil)), wantUnauthenticated: true, }, { name: "when the end user password has the wrong case (passwords are compared as case-sensitive)", username: "pinny", password: strings.ToUpper(pinnyPassword), - provider: provider(nil), + provider: upstreamldap.New(*providerConfig(nil)), wantUnauthenticated: true, }, { name: "when the end user username is wrong", username: "wrong-username", password: pinnyPassword, - provider: provider(nil), + provider: upstreamldap.New(*providerConfig(nil)), wantUnauthenticated: true, }, { name: "when the user search filter does not compile", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "*" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "*" })), wantError: `error searching for user "pinny": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`, }, { name: "when there are too many search results for the user", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "objectClass=*" // overly broad search filter - }), + })), wantError: `error searching for user "pinny": LDAP Result Code 4 "Size Limit Exceeded": `, }, { name: "when the server is unreachable", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.Host = "127.0.0.1:" + unusedHostPort }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.Host = "127.0.0.1:" + unusedHostPort })), wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:%s: connect: connection refused`, unusedHostPort, unusedHostPort), }, { name: "when the server is not parsable", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.Host = "too:many:ports" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.Host = "too:many:ports" })), wantError: `error dialing host "too:many:ports": LDAP Result Code 200 "Network Error": address too:many:ports: too many colons in address`, }, { name: "when the CA bundle is not parsable", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.CABundle = []byte("invalid-pem") }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.CABundle = []byte("invalid-pem") })), wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, ldapHostPort), }, { name: "when the CA bundle does not cause the host to be trusted", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.CABundle = nil }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.CABundle = nil })), wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, ldapHostPort), }, { name: "when the UsernameAttribute attribute has multiple values in the entry", username: "wally.ldap@example.com", password: "unused-because-error-is-before-bind", - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "mail" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "mail" })), wantError: `found 2 values for attribute "mail" while searching for user "wally.ldap@example.com", but expected 1 result`, }, { name: "when the UIDAttribute attribute has multiple values in the entry", username: "wally", password: "unused-because-error-is-before-bind", - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "mail" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "mail" })), wantError: `found 2 values for attribute "mail" while searching for user "wally", but expected 1 result`, }, { name: "when the UsernameAttribute attribute is not found in the entry", username: "wally", password: "unused-because-error-is-before-bind", - provider: provider(func(p *upstreamldap.Provider) { + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "cn={}" p.UserSearch.UsernameAttribute = "attr-does-not-exist" - }), + })), wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`, }, { name: "when the UIDAttribute attribute is not found in the entry", username: "wally", password: "unused-because-error-is-before-bind", - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "attr-does-not-exist" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "attr-does-not-exist" })), wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`, }, { name: "when the UsernameAttribute has the wrong case", username: "Seal", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "SN" }), // this is case-sensitive + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "SN" })), // this is case-sensitive wantError: `found 0 values for attribute "SN" while searching for user "Seal", but expected 1 result`, }, { name: "when the UIDAttribute has the wrong case", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "SN" }), // this is case-sensitive + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "SN" })), // this is case-sensitive wantError: `found 0 values for attribute "SN" while searching for user "pinny", but expected 1 result`, }, { name: "when the UsernameAttribute is DN and has the wrong case", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "DN" // dn must be lower-case p.UserSearch.Filter = "cn={}" - }), + })), wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`, }, { name: "when the UIDAttribute is DN and has the wrong case", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "DN" // dn must be lower-case - }), + })), wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`, }, { name: "when the search base is invalid", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "invalid-base" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "invalid-base" })), wantError: `error searching for user "pinny": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, }, { name: "when the search base does not exist", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" })), wantError: `error searching for user "pinny": LDAP Result Code 32 "No Such Object": `, }, { name: "when the search base causes no search results", username: "pinny", password: pinnyPassword, - provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" }), + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" })), wantUnauthenticated: true, }, { name: "when there is no username specified", username: "", password: pinnyPassword, - provider: provider(nil), + provider: upstreamldap.New(*providerConfig(nil)), wantUnauthenticated: true, }, { name: "when there is no password specified", username: "pinny", password: "", - provider: provider(nil), + provider: upstreamldap.New(*providerConfig(nil)), wantError: `error binding for user "pinny" using provided password against DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 206 "Empty password not allowed by the client": ldap: empty password not allowed by the client`, }, { name: "when the user has no password in their entry", username: "olive", password: "anything", - provider: provider(nil), + provider: upstreamldap.New(*providerConfig(nil)), wantUnauthenticated: true, }, } @@ -389,6 +377,75 @@ func TestLDAPSearch(t *testing.T) { } } +func TestSimultaneousRequestsOnSingleProvider(t *testing.T) { + env := library.IntegrationEnv(t) + + // Note that these tests depend on the values hard-coded in the LDIF file in test/deploy/tools/ldap.yaml. + // It requires the test LDAP server from the tools deployment. + if len(env.ToolsNamespace) == 0 { + t.Skip("Skipping test because it requires the test LDAP server in the tools namespace.") + } + + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(func() { + cancelFunc() // this will send SIGKILL to the subprocess, just in case + }) + + ldapHostPort := findRecentlyUnusedLocalhostPorts(t, 1)[0] + + // Expose the the test LDAP server's TLS port on the localhost. + startKubectlPortForward(ctx, t, ldapHostPort, "ldaps", "ldap", env.ToolsNamespace) + + provider := upstreamldap.New(*defaultProviderConfig(env, ldapHostPort)) + + // Making multiple simultaneous requests on the same upstreamldap.Provider instance should all succeed + // without triggering the race detector. + iterations := 150 + resultCh := make(chan authUserResult, iterations) + for i := 0; i < iterations; i++ { + go func() { + authResponse, authenticated, err := provider.AuthenticateUser(ctx, + env.SupervisorUpstreamLDAP.TestUserCN, env.SupervisorUpstreamLDAP.TestUserPassword, + ) + resultCh <- authUserResult{ + response: authResponse, + authenticated: authenticated, + err: err, + } + }() + } + for i := 0; i < iterations; i++ { + result := <-resultCh + require.NoError(t, result.err) + require.True(t, result.authenticated, "expected the user to be authenticated, but they were not") + require.Equal(t, &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, result.response) + } +} + +type authUserResult struct { + response *authenticator.Response + authenticated bool + err error +} + +func defaultProviderConfig(env *library.TestEnv, ldapHostPort string) *upstreamldap.ProviderConfig { + return &upstreamldap.ProviderConfig{ + Name: "test-ldap-provider", + Host: "127.0.0.1:" + ldapHostPort, + CABundle: []byte(env.SupervisorUpstreamLDAP.CABundle), + BindUsername: "cn=admin,dc=pinniped,dc=dev", + BindPassword: "password", + UserSearch: upstreamldap.UserSearchConfig{ + Base: "ou=users,dc=pinniped,dc=dev", + Filter: "", // defaults to UsernameAttribute={}, i.e. "cn={}" in this case + UsernameAttribute: "cn", + UIDAttribute: "uidNumber", + }, + } +} + func startKubectlPortForward(ctx context.Context, t *testing.T, hostPort, remotePort, serviceName, namespace string) { t.Helper() startLongRunningCommandAndWaitForInitialOutput(ctx, t, From b9ce84fd68c980fad10e96b016dfb6b37df01c15 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 15 Apr 2021 14:44:43 -0700 Subject: [PATCH 26/59] Test the LDAP config by connecting to the server in the controller --- .../upstreamwatcher/ldap_upstream_watcher.go | 59 ++++-- .../ldap_upstream_watcher_test.go | 171 ++++++++++++++---- internal/upstreamldap/upstreamldap.go | 36 +++- internal/upstreamldap/upstreamldap_test.go | 95 ++++++++++ 4 files changed, 307 insertions(+), 54 deletions(-) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index c119ac57..353267c9 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -8,6 +8,7 @@ import ( "crypto/x509" "encoding/base64" "fmt" + "time" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -31,7 +32,9 @@ const ( // Constants related to conditions. typeBindSecretValid = "BindSecretValid" - tlsConfigurationValid = "TLSConfigurationValid" + typeTLSConfigurationValid = "TLSConfigurationValid" + typeLDAPConnectionValid = "LDAPConnectionValid" + reasonLDAPConnectionError = "LDAPConnectionError" noTLSConfigurationMessage = "no TLS configuration provided" loadedTLSConfigurationMessage = "loaded TLS configuration" ) @@ -118,18 +121,26 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * }, Dialer: c.ldapDialer, } - conditions := []*v1alpha1.Condition{ - c.validateSecret(upstream, config), - c.validateTLSConfig(upstream, config), + + conditions := []*v1alpha1.Condition{} + secretValidCondition := c.validateSecret(upstream, config) + tlsValidCondition := c.validateTLSConfig(upstream, config) + conditions = append(conditions, secretValidCondition, tlsValidCondition) + + // No point in trying to connect to the server if the config was already determined to be invalid. + if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { + conditions = append(conditions, c.validateFinishedConfig(ctx, config)) } + hadErrorCondition := c.updateStatus(ctx, upstream, conditions) if hadErrorCondition { return nil } + return upstreamldap.New(*config) } -func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.ProviderConfig) *v1alpha1.Condition { +func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { tlsSpec := upstream.Spec.TLS if tlsSpec == nil { return c.validTLSCondition(noTLSConfigurationMessage) @@ -149,13 +160,37 @@ func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentit return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", errNoCertificates)) } - result.CABundle = bundle + config.CABundle = bundle return c.validTLSCondition(loadedTLSConfigurationMessage) } +func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { + ldapProvider := upstreamldap.New(*config) + + testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, 60*time.Second) + defer cancelFunc() + + err := ldapProvider.TestConnection(testConnectionTimeout) + if err != nil { + return &v1alpha1.Condition{ + Type: typeLDAPConnectionValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonLDAPConnectionError, + Message: fmt.Sprintf(`could not successfully connect to "%s" and bind as user "%s: %s`, config.Host, config.BindUsername, err.Error()), + } + } + + return &v1alpha1.Condition{ + Type: typeLDAPConnectionValid, + Status: v1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, config.Host, config.BindUsername), + } +} + func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Condition { return &v1alpha1.Condition{ - Type: tlsConfigurationValid, + Type: typeTLSConfigurationValid, Status: v1alpha1.ConditionTrue, Reason: reasonSuccess, Message: message, @@ -164,14 +199,14 @@ func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Cond func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Condition { return &v1alpha1.Condition{ - Type: tlsConfigurationValid, + Type: typeTLSConfigurationValid, Status: v1alpha1.ConditionFalse, Reason: reasonInvalidTLSConfig, Message: message, } } -func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, result *upstreamldap.ProviderConfig) *v1alpha1.Condition { +func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { secretName := upstream.Spec.Bind.SecretName secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) @@ -193,9 +228,9 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr } } - result.BindUsername = string(secret.Data[corev1.BasicAuthUsernameKey]) - result.BindPassword = string(secret.Data[corev1.BasicAuthPasswordKey]) - if len(result.BindUsername) == 0 || len(result.BindPassword) == 0 { + config.BindUsername = string(secret.Data[corev1.BasicAuthUsernameKey]) + config.BindPassword = string(secret.Data[corev1.BasicAuthPasswordKey]) + if len(config.BindUsername) == 0 || len(config.BindPassword) == 0 { return &v1alpha1.Condition{ Type: typeBindSecretValid, Status: v1alpha1.ConditionFalse, diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index ca362db9..0a2bdbe0 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -6,11 +6,13 @@ package upstreamwatcher import ( "context" "encoding/base64" + "errors" "fmt" "sort" "testing" "time" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -23,6 +25,7 @@ import ( pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/mocks/mockldapconn" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/upstreamldap" @@ -165,13 +168,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { testCABundle := testCA.Bundle() testCABundleBase64Encoded := base64.StdEncoding.EncodeToString(testCABundle) - successfulDialer := &comparableDialer{ - f: func(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) { - // TODO return a fake implementation of upstreamldap.Conn, or return an error for testing errors - return nil, nil - }, - } - validUpstream := &v1alpha1.LDAPIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, Spec: v1alpha1.LDAPIdentityProviderSpec{ @@ -206,30 +202,35 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { UsernameAttribute: testUsernameAttrName, UIDAttribute: testUIDAttrName, }, - Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through } tests := []struct { name string inputUpstreams []runtime.Object inputSecrets []runtime.Object - ldapDialer upstreamldap.LDAPDialer + setupMocks func(conn *mockldapconn.MockConn) + dialError error wantErr string wantResultingCache []*upstreamldap.ProviderConfig wantResultingUpstreams []v1alpha1.LDAPIdentityProvider }{ { - name: "no LDAPIdentityProvider upstreams clears the cache", + name: "no LDAPIdentityProvider upstreams clears the cache", + wantResultingCache: []*upstreamldap.ProviderConfig{}, }, { name: "one valid upstream updates the cache to include only that upstream", - ldapDialer: successfulDialer, inputUpstreams: []runtime.Object{validUpstream}, inputSecrets: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, @@ -244,6 +245,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: "loaded bind secret", ObservedGeneration: 1234, }, + { + Type: "LDAPConnectionValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), + ObservedGeneration: 1234, + }, { Type: "TLSConfigurationValid", Status: "True", @@ -258,7 +267,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, { name: "missing secret", - ldapDialer: successfulDialer, inputUpstreams: []runtime.Object{validUpstream}, inputSecrets: []runtime.Object{}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), @@ -290,7 +298,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, { name: "secret has wrong type", - ldapDialer: successfulDialer, inputUpstreams: []runtime.Object{validUpstream}, inputSecrets: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, @@ -326,7 +333,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, { name: "secret is missing key", - ldapDialer: successfulDialer, inputUpstreams: []runtime.Object{validUpstream}, inputSecrets: []runtime.Object{&corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, @@ -360,8 +366,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }}, }, { - name: "CertificateAuthorityData is not base64 encoded", - ldapDialer: successfulDialer, + name: "CertificateAuthorityData is not base64 encoded", inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = "this-is-not-base64-encoded" })}, @@ -398,8 +403,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }}, }, { - name: "CertificateAuthorityData is not valid pem data", - ldapDialer: successfulDialer, + name: "CertificateAuthorityData is not valid pem data", inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = base64.StdEncoding.EncodeToString([]byte("this is not pem data")) })}, @@ -436,8 +440,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }}, }, { - name: "nil TLS configuration", - ldapDialer: successfulDialer, + name: "nil TLS configuration is valid", inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS = nil })}, @@ -446,6 +449,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, @@ -459,7 +467,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { UsernameAttribute: testUsernameAttrName, UIDAttribute: testUIDAttrName, }, - Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through }, }, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ @@ -475,6 +482,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: "loaded bind secret", ObservedGeneration: 1234, }, + { + Type: "LDAPConnectionValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), + ObservedGeneration: 1234, + }, { Type: "TLSConfigurationValid", Status: "True", @@ -488,8 +503,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }}, }, { - name: "non-nil TLS configuration with empty CertificateAuthorityData", - ldapDialer: successfulDialer, + name: "non-nil TLS configuration with empty CertificateAuthorityData is valid", inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = "" })}, @@ -498,6 +512,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, wantResultingCache: []*upstreamldap.ProviderConfig{ { Name: testName, @@ -511,7 +530,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { UsernameAttribute: testUsernameAttrName, UIDAttribute: testUIDAttrName, }, - Dialer: successfulDialer, // the dialer passed to the controller's constructor should have been passed through }, }, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ @@ -527,6 +545,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: "loaded bind secret", ObservedGeneration: 1234, }, + { + Type: "LDAPConnectionValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), + ObservedGeneration: 1234, + }, { Type: "TLSConfigurationValid", Status: "True", @@ -540,8 +566,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }}, }, { - name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", - ldapDialer: successfulDialer, + name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", inputUpstreams: []runtime.Object{validUpstream, modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Name = "other-upstream" upstream.Generation = 42 @@ -552,6 +577,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Type: corev1.SecretTypeBasicAuth, Data: testValidSecretData, }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind for the one valid upstream configuration. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{ @@ -592,6 +622,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: "loaded bind secret", ObservedGeneration: 1234, }, + { + Type: "LDAPConnectionValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), + ObservedGeneration: 1234, + }, { Type: "TLSConfigurationValid", Status: "True", @@ -605,11 +643,62 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }, + { + name: "when testing the connection to the LDAP server fails then the upstream is not added to the cache", + inputUpstreams: []runtime.Object{validUpstream}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1).Return(errors.New("some bind error")) + conn.EXPECT().Close().Times(1) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: 1234, + }, + { + Type: "LDAPConnectionValid", + Status: "False", + LastTransitionTime: now, + Reason: "LDAPConnectionError", + Message: fmt.Sprintf( + `could not successfully connect to "%s" and bind as user "%s: error binding as "%s": some bind error`, + testHost, testBindUsername, testBindUsername), + ObservedGeneration: 1234, + }, + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() + fakePinnipedClient := pinnipedfake.NewSimpleClientset(tt.inputUpstreams...) pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClient, 0) fakeKubeClient := fake.NewSimpleClientset(tt.inputSecrets...) @@ -619,9 +708,24 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}), }) + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + conn := mockldapconn.NewMockConn(ctrl) + if tt.setupMocks != nil { + tt.setupMocks(conn) + } + + dialer := &comparableDialer{f: upstreamldap.LDAPDialerFunc(func(ctx context.Context, _ string) (upstreamldap.Conn, error) { + if tt.dialError != nil { + return nil, tt.dialError + } + return conn, nil + })} + controller := NewLDAPUpstreamWatcherController( cache, - successfulDialer, + dialer, fakePinnipedClient, pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), kubeInformers.Core().V1().Secrets(), @@ -647,7 +751,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) for i := range actualIDPList { actualIDP := actualIDPList[i].(*upstreamldap.Provider) - require.Equal(t, *tt.wantResultingCache[i], actualIDP.GetConfig()) + copyOfExpectedValue := *tt.wantResultingCache[i] // copy before edit to avoid race because these tests are run in parallel + // The dialer that was passed in to the controller's constructor should always have been + // passed through to the provider. + copyOfExpectedValue.Dialer = dialer + require.Equal(t, copyOfExpectedValue, actualIDP.GetConfig()) } actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().LDAPIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) @@ -660,13 +768,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { // Require each separately to get a nice diff when the test fails. require.Equal(t, tt.wantResultingUpstreams[i], normalizedActualUpstreams[i]) } - - // Running the sync() a second time should be idempotent, and should return the same error. - if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { - require.EqualError(t, err, tt.wantErr) - } else { - require.NoError(t, err) - } }) } } diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index f27551c7..e7694cf5 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -182,10 +182,24 @@ func (p *Provider) GetURL() string { // TestConnection provides a method for testing the connection and bind settings. It performs a dial and bind // and returns any errors that we encountered. -func (p *Provider) TestConnection(ctx context.Context) (*authenticator.Response, error) { - _, _ = p.dial(ctx) - // TODO implement me - return nil, nil +func (p *Provider) TestConnection(ctx context.Context) error { + err := p.validateConfig() + if err != nil { + return err + } + + conn, err := p.dial(ctx) + if err != nil { + return 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 fmt.Errorf(`error binding as "%s": %w`, p.c.BindUsername, err) + } + + return nil } // TestAuthenticateUser provides a method for testing all of the Provider settings in a kind of dry run of @@ -199,9 +213,9 @@ func (p *Provider) TestAuthenticateUser(ctx context.Context, testUsername string // Authenticate a 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) { - if p.c.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.c.UserSearch.Filter) == 0 { - // LDAP search filters do not allow searching by DN. - return nil, false, fmt.Errorf(`must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`) + err := p.validateConfig() + if err != nil { + return nil, false, err } if len(username) == 0 { @@ -239,6 +253,14 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri return response, true, nil } +func (p *Provider) validateConfig() error { + if p.c.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.c.UserSearch.Filter) == 0 { + // LDAP search filters do not allow searching by DN, so we would have no reasonable default for Filter. + return fmt.Errorf(`must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`) + } + return nil +} + func (p *Provider) searchAndBindUser(conn Conn, username string, password string) (string, string, error) { searchResult, err := conn.Search(p.userSearchRequest(username)) if err != nil { diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index b6b21846..c2cb7de6 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -608,6 +608,101 @@ func TestAuthenticateUser(t *testing.T) { } } +func TestTestConnection(t *testing.T) { + providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig { + config := &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 + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: UserSearchConfig{}, // not used by TestConnection + } + if editFunc != nil { + editFunc(config) + } + return config + } + + tests := []struct { + name string + providerConfig *ProviderConfig + setupMocks func(conn *mockldapconn.MockConn) + dialError error + wantError string + wantToSkipDial bool + }{ + { + name: "happy path", + providerConfig: providerConfig(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + }, + { + name: "when dial fails", + providerConfig: providerConfig(nil), + dialError: errors.New("some dial error"), + wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost), + }, + { + name: "when binding as the bind user returns an error", + providerConfig: providerConfig(nil), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`error binding as "%s": some bind error`, testBindUsername), + }, + { + name: "when the config is invalid", + providerConfig: providerConfig(func(p *ProviderConfig) { + // This particular combination of options is not allowed. + p.UserSearch.UsernameAttribute = "dn" + p.UserSearch.Filter = "" + }), + wantToSkipDial: true, + wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`, + }, + } + + for _, test := range tests { + tt := test + 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 + tt.providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) { + dialWasAttempted = true + require.Equal(t, tt.providerConfig.Host, hostAndPort) + if tt.dialError != nil { + return nil, tt.dialError + } + return conn, nil + }) + + provider := New(*tt.providerConfig) + err := provider.TestConnection(context.Background()) + + require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) + + switch { + case tt.wantError != "": + require.EqualError(t, err, tt.wantError) + default: + require.NoError(t, err) + } + }) + } +} + func TestGetConfig(t *testing.T) { c := ProviderConfig{ Name: "original-provider-name", From 8e438e22e959ae6be1fef6d0f003fe26b09a346f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 15 Apr 2021 16:46:27 -0700 Subject: [PATCH 27/59] Only test the server connection when the spec has changed This early version of the controller is not intended to act as an ongoing health check for your upstream LDAP server. It will connect to the LDAP server to essentially "lint" your configuration once. It will do it again only when you change your configuration. To account for transient errors, it will keep trying to connect to the server until it succeeds once. This commit does not include looking for changes in the associated bind user username/password Secret. --- .../upstreamwatcher/ldap_upstream_watcher.go | 24 +- .../ldap_upstream_watcher_test.go | 320 +++++++++--------- 2 files changed, 174 insertions(+), 170 deletions(-) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index 353267c9..fd76095a 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -129,7 +129,11 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * // No point in trying to connect to the server if the config was already determined to be invalid. if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { - conditions = append(conditions, c.validateFinishedConfig(ctx, config)) + finishedConfigCondition := c.validateFinishedConfig(ctx, upstream, config) + // nil when there is no need to update this condition. + if finishedConfigCondition != nil { + conditions = append(conditions, finishedConfigCondition) + } } hadErrorCondition := c.updateStatus(ctx, upstream, conditions) @@ -164,10 +168,14 @@ func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentit return c.validTLSCondition(loadedTLSConfigurationMessage) } -func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { +func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { ldapProvider := upstreamldap.New(*config) - testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, 60*time.Second) + if alreadyValidatedFinishedConfigForThisSpecGeneration(upstream) { + return nil + } + + testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, 90*time.Second) defer cancelFunc() err := ldapProvider.TestConnection(testConnectionTimeout) @@ -188,6 +196,16 @@ func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, conf } } +func alreadyValidatedFinishedConfigForThisSpecGeneration(upstream *v1alpha1.LDAPIdentityProvider) bool { + currentGeneration := upstream.Generation + for _, c := range upstream.Status.Conditions { + if c.Type == typeLDAPConnectionValid && c.Status == v1alpha1.ConditionTrue && c.ObservedGeneration == currentGeneration { + return true + } + } + return false +} + func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Condition { return &v1alpha1.Condition{ Type: typeTLSConfigurationValid, diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index 0a2bdbe0..c33ceebd 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -184,7 +184,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, } - modifiedCopyOfValidUpstream := func(editFunc func(*v1alpha1.LDAPIdentityProvider)) *v1alpha1.LDAPIdentityProvider { + editedValidUpstream := func(editFunc func(*v1alpha1.LDAPIdentityProvider)) *v1alpha1.LDAPIdentityProvider { deepCopy := validUpstream.DeepCopy() editFunc(deepCopy) return deepCopy @@ -204,6 +204,44 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, } + bindSecretValidTrueCondition := func(gen int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: gen, + } + } + ldapConnectionValidTrueCondition := func(gen int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "LDAPConnectionValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), + ObservedGeneration: gen, + } + } + tlsConfigurationValidLoadedTrueCondition := func(gen int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded TLS configuration", + ObservedGeneration: gen, + } + } + allConditionsTrue := func(gen int64) []v1alpha1.Condition { + return []v1alpha1.Condition{ + bindSecretValidTrueCondition(gen), + ldapConnectionValidTrueCondition(gen), + tlsConfigurationValidLoadedTrueCondition(gen), + } + } + tests := []struct { name string inputUpstreams []runtime.Object @@ -235,33 +273,8 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ - Phase: "Ready", - Conditions: []v1alpha1.Condition{ - { - Type: "BindSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded bind secret", - ObservedGeneration: 1234, - }, - { - Type: "LDAPConnectionValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), - ObservedGeneration: 1234, - }, - { - Type: "TLSConfigurationValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded TLS configuration", - ObservedGeneration: 1234, - }, - }, + Phase: "Ready", + Conditions: allConditionsTrue(1234), }, }}, }, @@ -284,14 +297,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`secret "%s" not found`, testSecretName), ObservedGeneration: 1234, }, - { - Type: "TLSConfigurationValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded TLS configuration", - ObservedGeneration: 1234, - }, + tlsConfigurationValidLoadedTrueCondition(1234), }, }, }}, @@ -319,14 +325,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`referenced Secret "%s" has wrong type "some-other-type" (should be "kubernetes.io/basic-auth")`, testSecretName), ObservedGeneration: 1234, }, - { - Type: "TLSConfigurationValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded TLS configuration", - ObservedGeneration: 1234, - }, + tlsConfigurationValidLoadedTrueCondition(1234), }, }, }}, @@ -353,21 +352,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`referenced Secret "%s" is missing required keys ["username" "password"]`, testSecretName), ObservedGeneration: 1234, }, - { - Type: "TLSConfigurationValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded TLS configuration", - ObservedGeneration: 1234, - }, + tlsConfigurationValidLoadedTrueCondition(1234), }, }, }}, }, { name: "CertificateAuthorityData is not base64 encoded", - inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = "this-is-not-base64-encoded" })}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -382,14 +374,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ - { - Type: "BindSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded bind secret", - ObservedGeneration: 1234, - }, + bindSecretValidTrueCondition(1234), { Type: "TLSConfigurationValid", Status: "False", @@ -404,7 +389,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, { name: "CertificateAuthorityData is not valid pem data", - inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = base64.StdEncoding.EncodeToString([]byte("this is not pem data")) })}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -419,14 +404,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ - { - Type: "BindSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded bind secret", - ObservedGeneration: 1234, - }, + bindSecretValidTrueCondition(1234), { Type: "TLSConfigurationValid", Status: "False", @@ -441,7 +419,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, { name: "nil TLS configuration is valid", - inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS = nil })}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -474,22 +452,8 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ - { - Type: "BindSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded bind secret", - ObservedGeneration: 1234, - }, - { - Type: "LDAPConnectionValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), - ObservedGeneration: 1234, - }, + bindSecretValidTrueCondition(1234), + ldapConnectionValidTrueCondition(1234), { Type: "TLSConfigurationValid", Status: "True", @@ -504,7 +468,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, { name: "non-nil TLS configuration with empty CertificateAuthorityData is valid", - inputUpstreams: []runtime.Object{modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = "" })}, inputSecrets: []runtime.Object{&corev1.Secret{ @@ -535,39 +499,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ - Phase: "Ready", - Conditions: []v1alpha1.Condition{ - { - Type: "BindSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded bind secret", - ObservedGeneration: 1234, - }, - { - Type: "LDAPConnectionValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), - ObservedGeneration: 1234, - }, - { - Type: "TLSConfigurationValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded TLS configuration", - ObservedGeneration: 1234, - }, - }, + Phase: "Ready", + Conditions: allConditionsTrue(1234), }, }}, }, { name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", - inputUpstreams: []runtime.Object{validUpstream, modifiedCopyOfValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + inputUpstreams: []runtime.Object{validUpstream, editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Name = "other-upstream" upstream.Generation = 42 upstream.Spec.Bind.SecretName = "non-existent-secret" @@ -598,47 +537,15 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Message: fmt.Sprintf(`secret "%s" not found`, "non-existent-secret"), ObservedGeneration: 42, }, - { - Type: "TLSConfigurationValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded TLS configuration", - ObservedGeneration: 42, - }, + tlsConfigurationValidLoadedTrueCondition(42), }, }, }, { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ - Phase: "Ready", - Conditions: []v1alpha1.Condition{ - { - Type: "BindSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded bind secret", - ObservedGeneration: 1234, - }, - { - Type: "LDAPConnectionValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), - ObservedGeneration: 1234, - }, - { - Type: "TLSConfigurationValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded TLS configuration", - ObservedGeneration: 1234, - }, - }, + Phase: "Ready", + Conditions: allConditionsTrue(1234), }, }, }, @@ -663,14 +570,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ - { - Type: "BindSecretValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded bind secret", - ObservedGeneration: 1234, - }, + bindSecretValidTrueCondition(1234), { Type: "LDAPConnectionValid", Status: "False", @@ -681,18 +581,104 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { testHost, testBindUsername, testBindUsername), ObservedGeneration: 1234, }, - { - Type: "TLSConfigurationValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: "loaded TLS configuration", - ObservedGeneration: 1234, - }, + tlsConfigurationValidLoadedTrueCondition(1234), }, }, }}, }, + { + name: "when the LDAP server connection was already validated for this resource generation, then do not validate it again", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + ldapConnectionValidTrueCondition(1234), + } + })}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called. + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234), + }, + }}, + }, + { + name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Generation = 1234 // current generation + upstream.Status.Conditions = []v1alpha1.Condition{ + { + Type: "LDAPConnectionValid", + Status: "False", + LastTransitionTime: now, + Reason: "LDAPConnectionError", + Message: "some-error-message", + ObservedGeneration: 1233, // older generation! + }, + } + })}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234), + }, + }}, + }, + { + name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + { + Type: "LDAPConnectionValid", + Status: "False", // failure! + LastTransitionTime: now, + Reason: "LDAPConnectionError", + Message: "some-error-message", + ObservedGeneration: 1234, // same (current) generation! + }, + } + })}, + inputSecrets: []runtime.Object{&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + }}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234), + }, + }}, + }, } for _, tt := range tests { tt := tt From 83085aa3d6e796a64dc13c9ba6499faa815130ab Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 15 Apr 2021 17:45:15 -0700 Subject: [PATCH 28/59] Retest the server connection when the bind Secret has changed Unfortunately, Secrets do not seem to have a Generation field, so we use the ResourceVersion field instead. This means that any change to the Secret will cause us to retry the connection to the LDAP server, even if the username and password fields in the Secret were not changed. Seems like an okay trade-off for this early draft of the controller compared to a more complex implementation. --- .../upstreamwatcher/ldap_upstream_watcher.go | 75 ++++++---- .../ldap_upstream_watcher_test.go | 130 ++++++++---------- 2 files changed, 105 insertions(+), 100 deletions(-) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index fd76095a..6a0796eb 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -8,6 +8,7 @@ import ( "crypto/x509" "encoding/base64" "fmt" + "regexp" "time" corev1 "k8s.io/api/core/v1" @@ -29,6 +30,7 @@ import ( const ( ldapControllerName = "ldap-upstream-observer" ldapBindAccountSecretType = corev1.SecretTypeBasicAuth + testLDAPConnectionTimeout = 90 * time.Second // Constants related to conditions. typeBindSecretValid = "BindSecretValid" @@ -39,6 +41,10 @@ const ( loadedTLSConfigurationMessage = "loaded TLS configuration" ) +var ( + secretVersionParser = regexp.MustCompile(` \[validated with Secret ".+" at version "(.+)"]`) +) + // UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. type UpstreamLDAPIdentityProviderICache interface { SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) @@ -123,13 +129,13 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * } conditions := []*v1alpha1.Condition{} - secretValidCondition := c.validateSecret(upstream, config) + secretValidCondition, currentSecretVersion := c.validateSecret(upstream, config) tlsValidCondition := c.validateTLSConfig(upstream, config) conditions = append(conditions, secretValidCondition, tlsValidCondition) // No point in trying to connect to the server if the config was already determined to be invalid. if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { - finishedConfigCondition := c.validateFinishedConfig(ctx, upstream, config) + finishedConfigCondition := c.validateFinishedConfig(ctx, upstream, config, currentSecretVersion) // nil when there is no need to update this condition. if finishedConfigCondition != nil { conditions = append(conditions, finishedConfigCondition) @@ -168,39 +174,50 @@ func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentit return c.validTLSCondition(loadedTLSConfigurationMessage) } -func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { +func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { ldapProvider := upstreamldap.New(*config) - if alreadyValidatedFinishedConfigForThisSpecGeneration(upstream) { + if hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion) { return nil } - testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, 90*time.Second) + testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout) defer cancelFunc() err := ldapProvider.TestConnection(testConnectionTimeout) if err != nil { return &v1alpha1.Condition{ - Type: typeLDAPConnectionValid, - Status: v1alpha1.ConditionFalse, - Reason: reasonLDAPConnectionError, - Message: fmt.Sprintf(`could not successfully connect to "%s" and bind as user "%s: %s`, config.Host, config.BindUsername, err.Error()), + Type: typeLDAPConnectionValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonLDAPConnectionError, + Message: fmt.Sprintf(`could not successfully connect to "%s" and bind as user "%s": %s`, + config.Host, config.BindUsername, err.Error()), } } return &v1alpha1.Condition{ - Type: typeLDAPConnectionValid, - Status: v1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, config.Host, config.BindUsername), + Type: typeLDAPConnectionValid, + Status: v1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + config.Host, config.BindUsername, upstream.Spec.Bind.SecretName, currentSecretVersion), } } -func alreadyValidatedFinishedConfigForThisSpecGeneration(upstream *v1alpha1.LDAPIdentityProvider) bool { +func hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool { currentGeneration := upstream.Generation for _, c := range upstream.Status.Conditions { if c.Type == typeLDAPConnectionValid && c.Status == v1alpha1.ConditionTrue && c.ObservedGeneration == currentGeneration { - return true + // Found a previously successful condition for the current spec generation. + // Now figure out which version of the bind Secret was used during that previous validation. + matches := secretVersionParser.FindStringSubmatch(c.Message) + if len(matches) != 2 { + continue + } + validatedSecretVersion := matches[1] + if validatedSecretVersion == currentSecretVersion { + return true + } } } return false @@ -224,7 +241,7 @@ func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Co } } -func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { +func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) (*v1alpha1.Condition, string) { secretName := upstream.Spec.Bind.SecretName secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) @@ -234,27 +251,29 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr Status: v1alpha1.ConditionFalse, Reason: reasonNotFound, Message: err.Error(), - } + }, "" } if secret.Type != corev1.SecretTypeBasicAuth { return &v1alpha1.Condition{ - Type: typeBindSecretValid, - Status: v1alpha1.ConditionFalse, - Reason: reasonWrongType, - Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", secretName, secret.Type, corev1.SecretTypeBasicAuth), - } + Type: typeBindSecretValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonWrongType, + Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", + secretName, secret.Type, corev1.SecretTypeBasicAuth), + }, secret.ResourceVersion } config.BindUsername = string(secret.Data[corev1.BasicAuthUsernameKey]) config.BindPassword = string(secret.Data[corev1.BasicAuthPasswordKey]) if len(config.BindUsername) == 0 || len(config.BindPassword) == 0 { return &v1alpha1.Condition{ - Type: typeBindSecretValid, - Status: v1alpha1.ConditionFalse, - Reason: reasonMissingKeys, - Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", secretName, []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey}), - } + Type: typeBindSecretValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingKeys, + Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", + secretName, []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey}), + }, secret.ResourceVersion } return &v1alpha1.Condition{ @@ -262,7 +281,7 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr Status: v1alpha1.ConditionTrue, Reason: reasonSuccess, Message: "loaded bind secret", - } + }, secret.ResourceVersion } func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) bool { diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index c33ceebd..b8af6d0c 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -214,13 +214,15 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObservedGeneration: gen, } } - ldapConnectionValidTrueCondition := func(gen int64) v1alpha1.Condition { + ldapConnectionValidTrueCondition := func(gen int64, secretVersion string) v1alpha1.Condition { return v1alpha1.Condition{ Type: "LDAPConnectionValid", Status: "True", LastTransitionTime: now, Reason: "Success", - Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s"`, testHost, testBindUsername), + Message: fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + testHost, testBindUsername, testSecretName, secretVersion), ObservedGeneration: gen, } } @@ -234,14 +236,22 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObservedGeneration: gen, } } - allConditionsTrue := func(gen int64) []v1alpha1.Condition { + allConditionsTrue := func(gen int64, secretVersion string) []v1alpha1.Condition { return []v1alpha1.Condition{ bindSecretValidTrueCondition(gen), - ldapConnectionValidTrueCondition(gen), + ldapConnectionValidTrueCondition(gen, secretVersion), tlsConfigurationValidLoadedTrueCondition(gen), } } + validBindUserSecret := func(secretVersion string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace, ResourceVersion: secretVersion}, + Type: corev1.SecretTypeBasicAuth, + Data: testValidSecretData, + } + } + tests := []struct { name string inputUpstreams []runtime.Object @@ -259,11 +269,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { { name: "one valid upstream updates the cache to include only that upstream", inputUpstreams: []runtime.Object{validUpstream}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -274,7 +280,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Ready", - Conditions: allConditionsTrue(1234), + Conditions: allConditionsTrue(1234, "4242"), }, }}, }, @@ -362,11 +368,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = "this-is-not-base64-encoded" })}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("")}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ @@ -392,11 +394,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = base64.StdEncoding.EncodeToString([]byte("this is not pem data")) })}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("")}, wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ @@ -422,11 +420,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS = nil })}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -453,7 +447,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Phase: "Ready", Conditions: []v1alpha1.Condition{ bindSecretValidTrueCondition(1234), - ldapConnectionValidTrueCondition(1234), + ldapConnectionValidTrueCondition(1234, "4242"), { Type: "TLSConfigurationValid", Status: "True", @@ -471,11 +465,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Spec.TLS.CertificateAuthorityData = "" })}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -500,7 +490,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Ready", - Conditions: allConditionsTrue(1234), + Conditions: allConditionsTrue(1234, "4242"), }, }}, }, @@ -511,11 +501,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { upstream.Generation = 42 upstream.Spec.Bind.SecretName = "non-existent-secret" })}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind for the one valid upstream configuration. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -545,7 +531,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Ready", - Conditions: allConditionsTrue(1234), + Conditions: allConditionsTrue(1234, "4242"), }, }, }, @@ -553,11 +539,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { { name: "when testing the connection to the LDAP server fails then the upstream is not added to the cache", inputUpstreams: []runtime.Object{validUpstream}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1).Return(errors.New("some bind error")) @@ -577,7 +559,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { LastTransitionTime: now, Reason: "LDAPConnectionError", Message: fmt.Sprintf( - `could not successfully connect to "%s" and bind as user "%s: error binding as "%s": some bind error`, + `could not successfully connect to "%s" and bind as user "%s": error binding as "%s": some bind error`, testHost, testBindUsername, testBindUsername), ObservedGeneration: 1234, }, @@ -587,18 +569,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }}, }, { - name: "when the LDAP server connection was already validated for this resource generation, then do not validate it again", + name: "when the LDAP server connection was already validated for the current resource generation and secret version, then do not validate it again", inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Generation = 1234 upstream.Status.Conditions = []v1alpha1.Condition{ - ldapConnectionValidTrueCondition(1234), + ldapConnectionValidTrueCondition(1234, "4242"), } })}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called. }, @@ -607,7 +585,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Ready", - Conditions: allConditionsTrue(1234), + Conditions: allConditionsTrue(1234, "4242"), }, }}, }, @@ -616,21 +594,10 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { upstream.Generation = 1234 // current generation upstream.Status.Conditions = []v1alpha1.Condition{ - { - Type: "LDAPConnectionValid", - Status: "False", - LastTransitionTime: now, - Reason: "LDAPConnectionError", - Message: "some-error-message", - ObservedGeneration: 1233, // older generation! - }, + ldapConnectionValidTrueCondition(1233, "4242"), // older spec generation! } })}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -641,7 +608,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Ready", - Conditions: allConditionsTrue(1234), + Conditions: allConditionsTrue(1234, "4242"), }, }}, }, @@ -660,11 +627,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, } })}, - inputSecrets: []runtime.Object{&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{Name: testSecretName, Namespace: testNamespace}, - Type: corev1.SecretTypeBasicAuth, - Data: testValidSecretData, - }}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -675,7 +638,30 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ Phase: "Ready", - Conditions: allConditionsTrue(1234), + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + }, + { + name: "when the LDAP server connection was already validated for this resource generation but the bind secret has changed, then try to validate it again", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + ldapConnectionValidTrueCondition(1234, "4241"), // same spec generation, old secret version + } + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), }, }}, }, From e9d5743845cfd1a2264e5bb1e97e6a4c831534f2 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 16 Apr 2021 14:04:05 -0700 Subject: [PATCH 29/59] Add authentication dry run validation to LDAPIdentityProvider Also force the LDAP server pod to restart whenever the LDIF file changes, so whenever you redeploy the tools deployment with a new test user password the server will be updated. --- .../types_ldapidentityprovider.go.tmpl | 22 ++ ...or.pinniped.dev_ldapidentityproviders.yaml | 32 +++ generated/1.17/README.adoc | 1 + .../v1alpha1/types_ldapidentityprovider.go | 22 ++ ...or.pinniped.dev_ldapidentityproviders.yaml | 32 +++ generated/1.18/README.adoc | 1 + .../v1alpha1/types_ldapidentityprovider.go | 22 ++ ...or.pinniped.dev_ldapidentityproviders.yaml | 32 +++ generated/1.19/README.adoc | 1 + .../v1alpha1/types_ldapidentityprovider.go | 22 ++ ...or.pinniped.dev_ldapidentityproviders.yaml | 32 +++ generated/1.20/README.adoc | 1 + .../v1alpha1/types_ldapidentityprovider.go | 22 ++ ...or.pinniped.dev_ldapidentityproviders.yaml | 32 +++ .../v1alpha1/types_ldapidentityprovider.go | 22 ++ .../upstreamwatcher/ldap_upstream_watcher.go | 70 +++++- .../ldap_upstream_watcher_test.go | 120 +++++++++ internal/upstreamldap/upstreamldap.go | 32 ++- internal/upstreamldap/upstreamldap_test.go | 140 +++++++---- test/deploy/tools/ldap.yaml | 231 +++++++++--------- test/integration/supervisor_login_test.go | 88 ++++++- 21 files changed, 802 insertions(+), 175 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index 0861c0de..c7a6b747 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -106,6 +106,28 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + + // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. + // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection + // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success + // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. + // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a + // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username + // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for + // that end user during the authentication. This will test all of the configuration options of the + // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the + // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships + // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate + // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". + // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able + // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch + // configuration were set up such that an end user should log in using their email address as their username, then + // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP + // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your + // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration + // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become + // disabled in the future. + DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d262f36f..c9924a16 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -69,6 +69,38 @@ spec: required: - secretName type: object + dryRunAuthenticationUsername: + description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's + configuration is validated. When DryRunAuthenticationUsername is + blank, the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server using the Host and TLS settings and also will + bind using the Bind settings. The success or failure of the connect + and bind will be reflected in the LDAPIdentityProvider's status + conditions array. When DryRunAuthenticationUsername is not blank, + the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server and performing a full dry run of authenticating + as the end user with the username specified by DryRunAuthenticationUsername. + The dry run will act as if the correct password were specified for + that end user during the authentication. This will test all of the + configuration options of the LDAPIdentityProvider. The success or + failure of the authentication dry run will be reflected in the LDAPIdentityProvider's + status conditions array, along with details of what username, UID, + and group memberships were selected for the specified user. If the + dry run fails, then that user would not be able to authenticate + in a real authentication situation either, so the LDAPIdentityProvider's + Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername + must be a valid username of a real user who should be able to authenticate + given all of the LDAPIdentityProvider's configuration. For example, + if the UserSearch configuration were set up such that an end user + should log in using their email address as their username, then + the DryRunAuthenticationUsername should be the actual email address + of a valid user who will be found in the LDAP server by the UserSearch + criteria. Once you have used DryRunAuthenticationUsername to validate + your LDAPIdentityProvider's configuration, you might choose to remove + the DryRunAuthenticationUsername configuration if you are concerned + that the user's LDAP account could change in the future, e.g. if + the account could become disabled in the future. + type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 8de20adc..27df8cb7 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -757,6 +757,7 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0861c0de..c7a6b747 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -106,6 +106,28 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + + // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. + // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection + // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success + // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. + // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a + // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username + // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for + // that end user during the authentication. This will test all of the configuration options of the + // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the + // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships + // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate + // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". + // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able + // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch + // configuration were set up such that an end user should log in using their email address as their username, then + // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP + // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your + // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration + // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become + // disabled in the future. + DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d262f36f..c9924a16 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -69,6 +69,38 @@ spec: required: - secretName type: object + dryRunAuthenticationUsername: + description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's + configuration is validated. When DryRunAuthenticationUsername is + blank, the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server using the Host and TLS settings and also will + bind using the Bind settings. The success or failure of the connect + and bind will be reflected in the LDAPIdentityProvider's status + conditions array. When DryRunAuthenticationUsername is not blank, + the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server and performing a full dry run of authenticating + as the end user with the username specified by DryRunAuthenticationUsername. + The dry run will act as if the correct password were specified for + that end user during the authentication. This will test all of the + configuration options of the LDAPIdentityProvider. The success or + failure of the authentication dry run will be reflected in the LDAPIdentityProvider's + status conditions array, along with details of what username, UID, + and group memberships were selected for the specified user. If the + dry run fails, then that user would not be able to authenticate + in a real authentication situation either, so the LDAPIdentityProvider's + Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername + must be a valid username of a real user who should be able to authenticate + given all of the LDAPIdentityProvider's configuration. For example, + if the UserSearch configuration were set up such that an end user + should log in using their email address as their username, then + the DryRunAuthenticationUsername should be the actual email address + of a valid user who will be found in the LDAP server by the UserSearch + criteria. Once you have used DryRunAuthenticationUsername to validate + your LDAPIdentityProvider's configuration, you might choose to remove + the DryRunAuthenticationUsername configuration if you are concerned + that the user's LDAP account could change in the future, e.g. if + the account could become disabled in the future. + type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 6de19496..2671cc14 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -757,6 +757,7 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0861c0de..c7a6b747 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -106,6 +106,28 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + + // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. + // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection + // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success + // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. + // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a + // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username + // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for + // that end user during the authentication. This will test all of the configuration options of the + // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the + // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships + // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate + // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". + // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able + // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch + // configuration were set up such that an end user should log in using their email address as their username, then + // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP + // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your + // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration + // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become + // disabled in the future. + DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d262f36f..c9924a16 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -69,6 +69,38 @@ spec: required: - secretName type: object + dryRunAuthenticationUsername: + description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's + configuration is validated. When DryRunAuthenticationUsername is + blank, the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server using the Host and TLS settings and also will + bind using the Bind settings. The success or failure of the connect + and bind will be reflected in the LDAPIdentityProvider's status + conditions array. When DryRunAuthenticationUsername is not blank, + the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server and performing a full dry run of authenticating + as the end user with the username specified by DryRunAuthenticationUsername. + The dry run will act as if the correct password were specified for + that end user during the authentication. This will test all of the + configuration options of the LDAPIdentityProvider. The success or + failure of the authentication dry run will be reflected in the LDAPIdentityProvider's + status conditions array, along with details of what username, UID, + and group memberships were selected for the specified user. If the + dry run fails, then that user would not be able to authenticate + in a real authentication situation either, so the LDAPIdentityProvider's + Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername + must be a valid username of a real user who should be able to authenticate + given all of the LDAPIdentityProvider's configuration. For example, + if the UserSearch configuration were set up such that an end user + should log in using their email address as their username, then + the DryRunAuthenticationUsername should be the actual email address + of a valid user who will be found in the LDAP server by the UserSearch + criteria. Once you have used DryRunAuthenticationUsername to validate + your LDAPIdentityProvider's configuration, you might choose to remove + the DryRunAuthenticationUsername configuration if you are concerned + that the user's LDAP account could change in the future, e.g. if + the account could become disabled in the future. + type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 37d2251c..00302c8c 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -757,6 +757,7 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0861c0de..c7a6b747 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -106,6 +106,28 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + + // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. + // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection + // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success + // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. + // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a + // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username + // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for + // that end user during the authentication. This will test all of the configuration options of the + // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the + // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships + // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate + // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". + // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able + // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch + // configuration were set up such that an end user should log in using their email address as their username, then + // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP + // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your + // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration + // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become + // disabled in the future. + DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d262f36f..c9924a16 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -69,6 +69,38 @@ spec: required: - secretName type: object + dryRunAuthenticationUsername: + description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's + configuration is validated. When DryRunAuthenticationUsername is + blank, the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server using the Host and TLS settings and also will + bind using the Bind settings. The success or failure of the connect + and bind will be reflected in the LDAPIdentityProvider's status + conditions array. When DryRunAuthenticationUsername is not blank, + the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server and performing a full dry run of authenticating + as the end user with the username specified by DryRunAuthenticationUsername. + The dry run will act as if the correct password were specified for + that end user during the authentication. This will test all of the + configuration options of the LDAPIdentityProvider. The success or + failure of the authentication dry run will be reflected in the LDAPIdentityProvider's + status conditions array, along with details of what username, UID, + and group memberships were selected for the specified user. If the + dry run fails, then that user would not be able to authenticate + in a real authentication situation either, so the LDAPIdentityProvider's + Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername + must be a valid username of a real user who should be able to authenticate + given all of the LDAPIdentityProvider's configuration. For example, + if the UserSearch configuration were set up such that an end user + should log in using their email address as their username, then + the DryRunAuthenticationUsername should be the actual email address + of a valid user who will be found in the LDAP server by the UserSearch + criteria. Once you have used DryRunAuthenticationUsername to validate + your LDAPIdentityProvider's configuration, you might choose to remove + the DryRunAuthenticationUsername configuration if you are concerned + that the user's LDAP account could change in the future, e.g. if + the account could become disabled in the future. + type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index a24ce0b5..fe6e5796 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -757,6 +757,7 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0861c0de..c7a6b747 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -106,6 +106,28 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + + // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. + // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection + // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success + // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. + // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a + // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username + // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for + // that end user during the authentication. This will test all of the configuration options of the + // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the + // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships + // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate + // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". + // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able + // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch + // configuration were set up such that an end user should log in using their email address as their username, then + // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP + // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your + // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration + // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become + // disabled in the future. + DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d262f36f..c9924a16 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -69,6 +69,38 @@ spec: required: - secretName type: object + dryRunAuthenticationUsername: + description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's + configuration is validated. When DryRunAuthenticationUsername is + blank, the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server using the Host and TLS settings and also will + bind using the Bind settings. The success or failure of the connect + and bind will be reflected in the LDAPIdentityProvider's status + conditions array. When DryRunAuthenticationUsername is not blank, + the LDAPIdentityProvider will be validated by opening a connection + to the LDAP server and performing a full dry run of authenticating + as the end user with the username specified by DryRunAuthenticationUsername. + The dry run will act as if the correct password were specified for + that end user during the authentication. This will test all of the + configuration options of the LDAPIdentityProvider. The success or + failure of the authentication dry run will be reflected in the LDAPIdentityProvider's + status conditions array, along with details of what username, UID, + and group memberships were selected for the specified user. If the + dry run fails, then that user would not be able to authenticate + in a real authentication situation either, so the LDAPIdentityProvider's + Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername + must be a valid username of a real user who should be able to authenticate + given all of the LDAPIdentityProvider's configuration. For example, + if the UserSearch configuration were set up such that an end user + should log in using their email address as their username, then + the DryRunAuthenticationUsername should be the actual email address + of a valid user who will be found in the LDAP server by the UserSearch + criteria. Once you have used DryRunAuthenticationUsername to validate + your LDAPIdentityProvider's configuration, you might choose to remove + the DryRunAuthenticationUsername configuration if you are concerned + that the user's LDAP account could change in the future, e.g. if + the account could become disabled in the future. + type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0861c0de..c7a6b747 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -106,6 +106,28 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + + // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. + // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection + // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success + // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. + // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a + // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username + // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for + // that end user during the authentication. This will test all of the configuration options of the + // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the + // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships + // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate + // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". + // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able + // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch + // configuration were set up such that an end user should log in using their email address as their username, then + // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP + // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your + // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration + // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become + // disabled in the future. + DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index 6a0796eb..dd0bd73f 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -33,12 +33,13 @@ const ( testLDAPConnectionTimeout = 90 * time.Second // Constants related to conditions. - typeBindSecretValid = "BindSecretValid" - typeTLSConfigurationValid = "TLSConfigurationValid" - typeLDAPConnectionValid = "LDAPConnectionValid" - reasonLDAPConnectionError = "LDAPConnectionError" - noTLSConfigurationMessage = "no TLS configuration provided" - loadedTLSConfigurationMessage = "loaded TLS configuration" + typeBindSecretValid = "BindSecretValid" + typeTLSConfigurationValid = "TLSConfigurationValid" + typeLDAPConnectionValid = "LDAPConnectionValid" + reasonLDAPConnectionError = "LDAPConnectionError" + reasonAuthenticationDryRunError = "AuthenticationDryRunError" + noTLSConfigurationMessage = "no TLS configuration provided" + loadedTLSConfigurationMessage = "loaded TLS configuration" ) var ( @@ -184,7 +185,21 @@ func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upst testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout) defer cancelFunc() - err := ldapProvider.TestConnection(testConnectionTimeout) + if len(upstream.Spec.DryRunAuthenticationUsername) > 0 { + return c.dryRunAuthentication(testConnectionTimeout, upstream, ldapProvider, currentSecretVersion) + } + + return c.testConnection(testConnectionTimeout, upstream, config, ldapProvider, currentSecretVersion) +} + +func (c *ldapWatcherController) testConnection( + ctx context.Context, + upstream *v1alpha1.LDAPIdentityProvider, + config *upstreamldap.ProviderConfig, + ldapProvider *upstreamldap.Provider, + currentSecretVersion string, +) *v1alpha1.Condition { + err := ldapProvider.TestConnection(ctx) if err != nil { return &v1alpha1.Condition{ Type: typeLDAPConnectionValid, @@ -204,6 +219,47 @@ func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upst } } +func (c *ldapWatcherController) dryRunAuthentication( + ctx context.Context, + upstream *v1alpha1.LDAPIdentityProvider, + ldapProvider *upstreamldap.Provider, + currentSecretVersion string, +) *v1alpha1.Condition { + authResponse, authenticated, err := ldapProvider.DryRunAuthenticateUser(ctx, upstream.Spec.DryRunAuthenticationUsername) + if err != nil { + return &v1alpha1.Condition{ + Type: typeLDAPConnectionValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonAuthenticationDryRunError, + Message: fmt.Sprintf(`failed authentication dry run for end user "%s": %s`, + upstream.Spec.DryRunAuthenticationUsername, err.Error()), + } + } + + if !authenticated { + // Since we aren't doing a real auth with a password that could be wrong, the only reason we should get + // an unauthenticated response without an error is when the username was wrong. + return &v1alpha1.Condition{ + Type: typeLDAPConnectionValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonAuthenticationDryRunError, + Message: fmt.Sprintf(`failed authentication dry run for end user "%s": user not found`, + upstream.Spec.DryRunAuthenticationUsername), + } + } + + return &v1alpha1.Condition{ + Type: typeLDAPConnectionValid, + Status: v1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: fmt.Sprintf( + `successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`, + upstream.Spec.DryRunAuthenticationUsername, + authResponse.User.GetName(), authResponse.User.GetUID(), + upstream.Spec.Bind.SecretName, currentSecretVersion), + } +} + func hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool { currentGeneration := upstream.Generation for _, c := range upstream.Status.Conditions { diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index b8af6d0c..e904f2a4 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/go-ldap/ldap/v3" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -665,7 +667,125 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }}, }, + { + name: "when DryRunAuthenticationUsername is specified and a successful dry run authentication is performed", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Spec.DryRunAuthenticationUsername = "endUserUsername" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a full auth dry run. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(gomock.Any()).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "testFoundUserDN", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUsernameAttrName, []string{"testDownstreamUsername"}), + ldap.NewEntryAttribute(testUIDAttrName, []string{"testDownstreamUID"}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + { + Type: "LDAPConnectionValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: fmt.Sprintf( + `successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`, + "endUserUsername", "testDownstreamUsername", "testDownstreamUID", testSecretName, "4242"), + ObservedGeneration: 1234, + }, + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + }, + { + name: "when DryRunAuthenticationUsername is specified and the dry run authentication returns an error", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Spec.DryRunAuthenticationUsername = "endUserUsername" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Failure during a full auth dry run. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(gomock.Any()).Return(nil, errors.New("some dry run error")).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + { + Type: "LDAPConnectionValid", + Status: "False", + LastTransitionTime: now, + Reason: "AuthenticationDryRunError", + Message: fmt.Sprintf( + `failed authentication dry run for end user "%s": error searching for user "%s": some dry run error`, + "endUserUsername", "endUserUsername"), + ObservedGeneration: 1234, + }, + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + }, + { + name: "when DryRunAuthenticationUsername is specified and the dry run authentication returns unauthenticated without an error", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { + upstream.Spec.DryRunAuthenticationUsername = "endUserUsername" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Failure during full auth dry run which will cause it to return unauthenticated instead of error. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(gomock.Any()).Return(&ldap.SearchResult{ + // No search results means the user did not enter a valid username, which is unauthenticated instead of error. + Entries: []*ldap.Entry{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.LDAPIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + { + Type: "LDAPConnectionValid", + Status: "False", + LastTransitionTime: now, + Reason: "AuthenticationDryRunError", + Message: fmt.Sprintf( + `failed authentication dry run for end user "%s": user not found`, + "endUserUsername"), + ObservedGeneration: 1234, + }, + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + }, } + for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index e7694cf5..ae296db5 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -202,17 +202,27 @@ func (p *Provider) TestConnection(ctx context.Context) error { return nil } -// TestAuthenticateUser provides a method for testing all of the Provider settings in a kind of dry run of -// authentication. 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 authenticator.Response values and the same errors that a real call to +// DryRunAuthenticateUser provides a method for testing all of the Provider settings in a kind of dry run of +// 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) TestAuthenticateUser(ctx context.Context, testUsername string) (*authenticator.Response, error) { - // TODO implement me - return nil, nil +func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticator.Response, bool, error) { + endUserBindFunc := func(conn Conn, foundUserDN string) error { + // Act as if the end user bind always succeeds. + return nil + } + return p.authenticateUserImpl(ctx, username, endUserBindFunc) } -// Authenticate a user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator. +// 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) { + 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) { err := p.validateConfig() if err != nil { return nil, false, err @@ -234,7 +244,7 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err) } - mappedUsername, mappedUID, err := p.searchAndBindUser(conn, username, password) + mappedUsername, mappedUID, err := p.searchAndBindUser(conn, username, bindFunc) if err != nil { return nil, false, err } @@ -261,7 +271,7 @@ func (p *Provider) validateConfig() error { return nil } -func (p *Provider) searchAndBindUser(conn Conn, username string, password string) (string, string, error) { +func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, error) { searchResult, err := conn.Search(p.userSearchRequest(username)) if err != nil { return "", "", fmt.Errorf(`error searching for user "%s": %w`, username, err) @@ -292,7 +302,7 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, password string } // Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername! - err = conn.Bind(userEntry.DN, password) + err = bindFunc(conn, userEntry.DN) if err != nil { plog.DebugErr("error binding for user (if this is not the expected dn for this username, please check the user search configuration)", err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN) @@ -344,7 +354,7 @@ func (p *Provider) userSearchFilter(username string) string { } func (p *Provider) escapeUsernameForSearchFilter(username string) string { - // The username is end-user input, so it should be escaped before being included in a search to prevent query injection. + // The username is end user input, so it should be escaped before being included in a search to prevent query injection. return ldap.EscapeFilter(username) } diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index c2cb7de6..73a42e08 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -42,7 +42,7 @@ var ( testUserSearchFilterInterpolated = fmt.Sprintf("(some-filter=%s-and-more-filter=%s)", testUpstreamUsername, testUpstreamUsername) ) -func TestAuthenticateUser(t *testing.T) { +func TestEndUserAuthentication(t *testing.T) { providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig { config := &ProviderConfig{ Name: "some-provider-name", @@ -82,23 +82,25 @@ func TestAuthenticateUser(t *testing.T) { } tests := []struct { - name string - username string - password string - providerConfig *ProviderConfig - setupMocks func(conn *mockldapconn.MockConn) - dialError error - wantError string - wantToSkipDial bool - wantAuthResponse *authenticator.Response - wantUnauthenticated bool + name string + username string + password string + providerConfig *ProviderConfig + searchMocks func(conn *mockldapconn.MockConn) + bindEndUserMocks func(conn *mockldapconn.MockConn) + dialError error + wantError string + wantToSkipDial bool + wantAuthResponse *authenticator.Response + wantUnauthenticated bool + skipDryRunAuthenticateUser bool // tests about when the end user bind fails don't make sense for DryRunAuthenticateUser() }{ { name: "happy path", username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -113,9 +115,11 @@ func TestAuthenticateUser(t *testing.T) { Referrals: []string{}, // note that we are not following referrals at this time Controls: []ldap.Control{}, // TODO are there any response controls that we need to be able to handle? }, nil).Times(1) - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) conn.EXPECT().Close().Times(1) }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + }, wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, @@ -131,7 +135,7 @@ func TestAuthenticateUser(t *testing.T) { providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.Filter = "(" + testUserSearchFilter + ")" }), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -144,9 +148,11 @@ func TestAuthenticateUser(t *testing.T) { }, }, }, nil).Times(1) - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) conn.EXPECT().Close().Times(1) }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + }, wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, @@ -162,7 +168,7 @@ func TestAuthenticateUser(t *testing.T) { providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.UsernameAttribute = "dn" }), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { r.Attributes = []string{testUserSearchUIDAttribute} @@ -176,9 +182,11 @@ func TestAuthenticateUser(t *testing.T) { }, }, }, nil).Times(1) - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) conn.EXPECT().Close().Times(1) }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + }, wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultDNValue, @@ -194,7 +202,7 @@ func TestAuthenticateUser(t *testing.T) { providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.UIDAttribute = "dn" }), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { r.Attributes = []string{testUserSearchUsernameAttribute} @@ -208,9 +216,11 @@ func TestAuthenticateUser(t *testing.T) { }, }, }, nil).Times(1) - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) conn.EXPECT().Close().Times(1) }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + }, wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, @@ -226,7 +236,7 @@ func TestAuthenticateUser(t *testing.T) { providerConfig: providerConfig(func(p *ProviderConfig) { p.UserSearch.Filter = "" }), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { r.Filter = "(" + testUserSearchUsernameAttribute + "=" + testUpstreamUsername + ")" @@ -241,9 +251,11 @@ func TestAuthenticateUser(t *testing.T) { }, }, }, nil).Times(1) - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) conn.EXPECT().Close().Times(1) }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + }, wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, @@ -257,7 +269,7 @@ func TestAuthenticateUser(t *testing.T) { username: `a&b|c(d)e\f*g`, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { r.Filter = fmt.Sprintf("(some-filter=%s-and-more-filter=%s)", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) @@ -272,9 +284,11 @@ func TestAuthenticateUser(t *testing.T) { }, }, }, nil).Times(1) - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) conn.EXPECT().Close().Times(1) }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + }, wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, @@ -307,7 +321,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1) conn.EXPECT().Close().Times(1) }, @@ -318,7 +332,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(nil, errors.New("some search error")).Times(1) conn.EXPECT().Close().Times(1) @@ -330,7 +344,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{}, @@ -344,7 +358,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -361,7 +375,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -377,7 +391,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -398,7 +412,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -423,7 +437,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -445,7 +459,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -466,7 +480,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -491,7 +505,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -513,7 +527,7 @@ func TestAuthenticateUser(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -526,17 +540,20 @@ func TestAuthenticateUser(t *testing.T) { }, }, }, nil).Times(1) - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(errors.New("some bind error")).Times(1) conn.EXPECT().Close().Times(1) }, - wantError: fmt.Sprintf(`error binding for user "%s" using provided password against DN "%s": some bind error`, testUpstreamUsername, testSearchResultDNValue), + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(errors.New("some bind error")).Times(1) + }, + skipDryRunAuthenticateUser: true, + wantError: fmt.Sprintf(`error binding for user "%s" using provided password against DN "%s": some bind error`, testUpstreamUsername, testSearchResultDNValue), }, { name: "when binding as the found user returns a specific invalid credentials error", username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(nil), - setupMocks: func(conn *mockldapconn.MockConn) { + searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -549,10 +566,13 @@ func TestAuthenticateUser(t *testing.T) { }, }, }, nil).Times(1) - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(errors.New(`LDAP Result Code 49 "Invalid Credentials": some bind error`)).Times(1) conn.EXPECT().Close().Times(1) }, - wantUnauthenticated: true, + wantUnauthenticated: true, + skipDryRunAuthenticateUser: true, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(errors.New(`LDAP Result Code 49 "Invalid Credentials": some bind error`)).Times(1) + }, }, { name: "when no username is specified", @@ -571,8 +591,11 @@ func TestAuthenticateUser(t *testing.T) { t.Cleanup(ctrl.Finish) conn := mockldapconn.NewMockConn(ctrl) - if tt.setupMocks != nil { - tt.setupMocks(conn) + if tt.searchMocks != nil { + tt.searchMocks(conn) + } + if tt.bindEndUserMocks != nil { + tt.bindEndUserMocks(conn) } dialWasAttempted := false @@ -586,10 +609,41 @@ func TestAuthenticateUser(t *testing.T) { }) provider := New(*tt.providerConfig) + authResponse, authenticated, err := provider.AuthenticateUser(context.Background(), tt.username, tt.password) - require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) + switch { + case tt.wantError != "": + require.EqualError(t, err, tt.wantError) + require.False(t, authenticated) + require.Nil(t, authResponse) + case tt.wantUnauthenticated: + require.NoError(t, err) + require.False(t, authenticated) + require.Nil(t, authResponse) + default: + require.NoError(t, err) + require.True(t, authenticated) + require.Equal(t, tt.wantAuthResponse, authResponse) + } + // DryRunAuthenticateUser() should have the same behavior as AuthenticateUser() except that it does not bind + // as the end user to confirm their password. Since it should behave the same, all of the same test cases + // apply, except for those which are specifically testing what happens when the end user bind fails. + if tt.skipDryRunAuthenticateUser { + return // move on to the next test + } + + // Reset some variables to get ready to call DryRunAuthenticateUser(). + dialWasAttempted = false + conn = mockldapconn.NewMockConn(ctrl) + if tt.searchMocks != nil { + tt.searchMocks(conn) + } + // Skip tt.bindEndUserMocks since DryRunAuthenticateUser() never binds as the end user. + + authResponse, authenticated, err = provider.DryRunAuthenticateUser(context.Background(), tt.username) + require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) switch { case tt.wantError != "": require.EqualError(t, err, tt.wantError) diff --git a/test/deploy/tools/ldap.yaml b/test/deploy/tools/ldap.yaml index e98abccb..affb7c61 100644 --- a/test/deploy/tools/ldap.yaml +++ b/test/deploy/tools/ldap.yaml @@ -2,6 +2,122 @@ #! SPDX-License-Identifier: Apache-2.0 #@ load("@ytt:data", "data") +#@ load("@ytt:sha256", "sha256") +#@ load("@ytt:yaml", "yaml") + +#@ def ldapLIDIF(): +#@yaml/text-templated-strings +ldap.ldif: | + # ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! *** + # Here's a good explanation of LDIF: + # https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system + + # pinniped.dev (organization, root) + dn: dc=pinniped,dc=dev + objectClass: dcObject + objectClass: organization + dc: pinniped + o: example + + # users, pinniped.dev (organization unit) + dn: ou=users,dc=pinniped,dc=dev + objectClass: organizationalUnit + ou: users + + # groups, pinniped.dev (organization unit) + dn: ou=groups,dc=pinniped,dc=dev + objectClass: organizationalUnit + ou: groups + + # beach-groups, groups, pinniped.dev (organization unit) + dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev + objectClass: organizationalUnit + ou: beach-groups + + # pinny, users, pinniped.dev (user) + dn: cn=pinny,ou=users,dc=pinniped,dc=dev + objectClass: inetOrgPerson + objectClass: posixAccount + objectClass: shadowAccount + cn: pinny + sn: Seal + givenName: Pinny + mail: pinny.ldap@example.com + userPassword: (@= data.values.pinny_ldap_password @) + uid: pinny + uidNumber: 1000 + gidNumber: 1000 + homeDirectory: /home/pinny + loginShell: /bin/bash + gecos: pinny-the-seal + + # wally, users, pinniped.dev (user without password) + dn: cn=wally,ou=users,dc=pinniped,dc=dev + objectClass: inetOrgPerson + objectClass: posixAccount + objectClass: shadowAccount + cn: wally + sn: Walrus + givenName: Wally + mail: wally.ldap@example.com + mail: wally.alternate@example.com + uid: wally + uidNumber: 1001 + gidNumber: 1001 + homeDirectory: /home/wally + loginShell: /bin/bash + gecos: wally-the-walrus + + # olive, users, pinniped.dev (user without password) + dn: cn=olive,ou=users,dc=pinniped,dc=dev + objectClass: inetOrgPerson + objectClass: posixAccount + objectClass: shadowAccount + cn: olive + sn: Boston Terrier + givenName: Olive + mail: olive.ldap@example.com + uid: olive + uidNumber: 1002 + gidNumber: 1002 + homeDirectory: /home/olive + loginShell: /bin/bash + gecos: olive-the-dog + + # ball-game-players, beach-groups, groups, pinniped.dev (group of users) + dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev + cn: ball-game-players + objectClass: groupOfNames + member: cn=pinny,ou=users,dc=pinniped,dc=dev + member: cn=olive,ou=users,dc=pinniped,dc=dev + + # seals, groups, pinniped.dev (group of users) + dn: cn=seals,ou=groups,dc=pinniped,dc=dev + cn: seals + objectClass: groupOfNames + member: cn=pinny,ou=users,dc=pinniped,dc=dev + + # walruses, groups, pinniped.dev (group of users) + dn: cn=walruses,ou=groups,dc=pinniped,dc=dev + cn: walruses + objectClass: groupOfNames + member: cn=wally,ou=users,dc=pinniped,dc=dev + + # pinnipeds, users, pinniped.dev (group of groups) + dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev + cn: pinnipeds + objectClass: groupOfNames + member: cn=seals,ou=groups,dc=pinniped,dc=dev + member: cn=walruses,ou=groups,dc=pinniped,dc=dev + + # mammals, groups, pinniped.dev (group of both groups and users) + dn: cn=mammals,ou=groups,dc=pinniped,dc=dev + cn: mammals + objectClass: groupOfNames + member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev + member: cn=olive,ou=users,dc=pinniped,dc=dev +#@ end + --- apiVersion: v1 kind: Secret @@ -9,117 +125,7 @@ metadata: name: ldap-ldif-files namespace: tools type: Opaque -stringData: - #@yaml/text-templated-strings - ldap.ldif: | - # ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! *** - # Here's a good explanation of LDIF: - # https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system - - # pinniped.dev (organization, root) - dn: dc=pinniped,dc=dev - objectClass: dcObject - objectClass: organization - dc: pinniped - o: example - - # users, pinniped.dev (organization unit) - dn: ou=users,dc=pinniped,dc=dev - objectClass: organizationalUnit - ou: users - - # groups, pinniped.dev (organization unit) - dn: ou=groups,dc=pinniped,dc=dev - objectClass: organizationalUnit - ou: groups - - # beach-groups, groups, pinniped.dev (organization unit) - dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev - objectClass: organizationalUnit - ou: beach-groups - - # pinny, users, pinniped.dev (user) - dn: cn=pinny,ou=users,dc=pinniped,dc=dev - objectClass: inetOrgPerson - objectClass: posixAccount - objectClass: shadowAccount - cn: pinny - sn: Seal - givenName: Pinny - mail: pinny.ldap@example.com - userPassword: (@= data.values.pinny_ldap_password @) - uid: pinny - uidNumber: 1000 - gidNumber: 1000 - homeDirectory: /home/pinny - loginShell: /bin/bash - gecos: pinny-the-seal - - # wally, users, pinniped.dev (user without password) - dn: cn=wally,ou=users,dc=pinniped,dc=dev - objectClass: inetOrgPerson - objectClass: posixAccount - objectClass: shadowAccount - cn: wally - sn: Walrus - givenName: Wally - mail: wally.ldap@example.com - mail: wally.alternate@example.com - uid: wally - uidNumber: 1001 - gidNumber: 1001 - homeDirectory: /home/wally - loginShell: /bin/bash - gecos: wally-the-walrus - - # olive, users, pinniped.dev (user without password) - dn: cn=olive,ou=users,dc=pinniped,dc=dev - objectClass: inetOrgPerson - objectClass: posixAccount - objectClass: shadowAccount - cn: olive - sn: Boston Terrier - givenName: Olive - mail: olive.ldap@example.com - uid: olive - uidNumber: 1002 - gidNumber: 1002 - homeDirectory: /home/olive - loginShell: /bin/bash - gecos: olive-the-dog - - # ball-game-players, beach-groups, groups, pinniped.dev (group of users) - dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev - cn: ball-game-players - objectClass: groupOfNames - member: cn=pinny,ou=users,dc=pinniped,dc=dev - member: cn=olive,ou=users,dc=pinniped,dc=dev - - # seals, groups, pinniped.dev (group of users) - dn: cn=seals,ou=groups,dc=pinniped,dc=dev - cn: seals - objectClass: groupOfNames - member: cn=pinny,ou=users,dc=pinniped,dc=dev - - # walruses, groups, pinniped.dev (group of users) - dn: cn=walruses,ou=groups,dc=pinniped,dc=dev - cn: walruses - objectClass: groupOfNames - member: cn=wally,ou=users,dc=pinniped,dc=dev - - # pinnipeds, users, pinniped.dev (group of groups) - dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev - cn: pinnipeds - objectClass: groupOfNames - member: cn=seals,ou=groups,dc=pinniped,dc=dev - member: cn=walruses,ou=groups,dc=pinniped,dc=dev - - # mammals, groups, pinniped.dev (group of both groups and users) - dn: cn=mammals,ou=groups,dc=pinniped,dc=dev - cn: mammals - objectClass: groupOfNames - member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev - member: cn=olive,ou=users,dc=pinniped,dc=dev +stringData: #@ ldapLIDIF() --- apiVersion: apps/v1 kind: Deployment @@ -137,6 +143,9 @@ spec: metadata: labels: app: ldap + annotations: + #! Cause the pod to get recreated whenever the LDIF file changes. + ldifConfigHash: #@ sha256.sum(yaml.encode(ldapLIDIF())) spec: containers: - name: ldap diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 2ee9f8b4..ba47ed0d 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -66,9 +66,8 @@ func TestSupervisorLogin(t *testing.T) { // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", }, - // TODO add more variations of this LDAP test to try using different user search filters and attributes { - name: "ldap", + name: "ldap with email as username and with dry run", createIDP: func(t *testing.T) { t.Helper() secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, @@ -77,7 +76,7 @@ func TestSupervisorLogin(t *testing.T) { v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, }, ) - library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ + ldapIDP := library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ Host: env.SupervisorUpstreamLDAP.Host, TLS: &idpv1alpha1.LDAPIdentityProviderTLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), @@ -93,7 +92,15 @@ func TestSupervisorLogin(t *testing.T) { UniqueID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, + DryRunAuthenticationUsername: env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, }, idpv1alpha1.LDAPPhaseReady) + expectedMsg := fmt.Sprintf( + `successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`, + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, + env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) }, requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorizationUsingLDAPIdentityProvider(t, @@ -110,6 +117,56 @@ func TestSupervisorLogin(t *testing.T) { // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), }, + { + name: "ldap with CN as username and without dry run", // try another variation of configuration options + createIDP: func(t *testing.T) { + t.Helper() + secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, + }, + ) + ldapIDP := library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.LDAPIdentityProviderTLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), + }, + Bind: idpv1alpha1.LDAPIdentityProviderBindSpec{ + SecretName: secret.Name, + }, + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearchSpec{ + Base: env.SupervisorUpstreamLDAP.UserSearchBase, + Filter: "cn={}", // try using a non-default search filter + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ + Username: "dn", // try using the user's DN as the downstream username + UniqueID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + }, + }, + DryRunAuthenticationUsername: "", // try without dry run + }, 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) + }, + requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { + requestAuthorizationUsingLDAPIdentityProvider(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login + httpClient, + ) + }, + // 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 + "?sub=" + env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue, + ), + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN), + }, } for _, test := range tests { test := test @@ -124,6 +181,31 @@ func TestSupervisorLogin(t *testing.T) { } } +func requireSuccessfulLDAPIdentityProviderConditions(t *testing.T, ldapIDP *idpv1alpha1.LDAPIdentityProvider, expectedLDAPConnectionValidMessage string) { + require.Len(t, ldapIDP.Status.Conditions, 3) + + conditionsSummary := [][]string{} + for _, condition := range ldapIDP.Status.Conditions { + conditionsSummary = append(conditionsSummary, []string{condition.Type, string(condition.Status), condition.Reason}) + t.Logf("Saw LDAPIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s", + condition.Type, string(condition.Status), condition.Reason, condition.Message) + switch condition.Type { + case "BindSecretValid": + require.Equal(t, "loaded bind secret", condition.Message) + case "TLSConfigurationValid": + require.Equal(t, "loaded TLS configuration", condition.Message) + case "LDAPConnectionValid": + require.Equal(t, expectedLDAPConnectionValidMessage, condition.Message) + } + } + + require.ElementsMatch(t, [][]string{ + {"BindSecretValid", "True", "Success"}, + {"TLSConfigurationValid", "True", "Success"}, + {"LDAPConnectionValid", "True", "Success"}, + }, conditionsSummary) +} + func testSupervisorLogin( t *testing.T, createIDP func(t *testing.T), From 4c2a0b4872619c9072268cc18d23e8a768a74f7c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 16 Apr 2021 18:30:31 -0700 Subject: [PATCH 30/59] Add new command-line flags to the `login oidc` command - Also some light prefactoring in login.go to make room for LDAP-style login, which is not implemented yet in this commit. TODOs are added. - And fix a test pollution problem in login_oidc_test.go where it was using a real on-disk CLI cache file, so the tests were polluted by the contents of that file and would sometimes cause each other to fail. --- cmd/pinniped/cmd/login_oidc.go | 55 ++++++++++----- cmd/pinniped/cmd/login_oidc_test.go | 78 ++++++++++++++++------ pkg/oidcclient/login.go | 100 +++++++++++++++++++++++++--- 3 files changed, 188 insertions(+), 45 deletions(-) diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 34ead8f8..0a1e2fe0 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -50,23 +50,25 @@ func oidcLoginCommandRealDeps() oidcLoginCommandDeps { } type oidcLoginFlags struct { - issuer string - clientID string - listenPort uint16 - scopes []string - skipBrowser bool - sessionCachePath string - caBundlePaths []string - caBundleData []string - debugSessionCache bool - requestAudience string - conciergeEnabled bool - conciergeAuthenticatorType string - conciergeAuthenticatorName string - conciergeEndpoint string - conciergeCABundle string - conciergeAPIGroupSuffix string - credentialCachePath string + issuer string + clientID string + listenPort uint16 + scopes []string + skipBrowser bool + sessionCachePath string + caBundlePaths []string + caBundleData []string + debugSessionCache bool + requestAudience string + conciergeEnabled bool + conciergeAuthenticatorType string + conciergeAuthenticatorName string + conciergeEndpoint string + conciergeCABundle string + conciergeAPIGroupSuffix string + credentialCachePath string + upstreamIdentityProviderName string + upstreamIdentityProviderType string } func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { @@ -98,6 +100,8 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)") + cmd.Flags().StringVar(&flags.upstreamIdentityProviderName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor") + cmd.Flags().StringVar(&flags.upstreamIdentityProviderType, "upstream-identity-provider-type", "oidc", "The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap')") mustMarkHidden(cmd, "debug-session-cache") mustMarkRequired(cmd, "issuer") @@ -137,6 +141,23 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin opts = append(opts, oidcclient.WithRequestAudience(flags.requestAudience)) } + if flags.upstreamIdentityProviderName != "" { + opts = append(opts, oidcclient.WithUpstreamIdentityProvider( + flags.upstreamIdentityProviderName, flags.upstreamIdentityProviderType)) + } + + switch flags.upstreamIdentityProviderType { + case "oidc": + // this is the default, so don't need to do anything + case "ldap": + opts = append(opts, oidcclient.WithLDAPUpstreamIdentityProvider()) + default: + // Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236 + return fmt.Errorf( + "--upstream-identity-provider-type value not recognized: %s (supported values: oidc, ldap)", + flags.upstreamIdentityProviderType) + } + var concierge *conciergeclient.Client if flags.conciergeEnabled { var err error diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 8472f6af..5f18fa05 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -56,23 +56,25 @@ func TestLoginOIDCCommand(t *testing.T) { oidc --issuer ISSUER [flags] Flags: - --ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated) - --ca-bundle-data strings Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated) - --client-id string OpenID Connect client ID (default "pinniped-cli") - --concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev") - --concierge-authenticator-name string Concierge authenticator name - --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') - --concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge - --concierge-endpoint string API base for the Concierge endpoint - --credential-cache string Path to cluster-specific credentials cache ("" disables the cache) (default "` + cfgDir + `/credentials.yaml") - --enable-concierge Use the Concierge to login - -h, --help help for oidc - --issuer string OpenID Connect issuer URL - --listen-port uint16 TCP port for localhost listener (authorization code flow only) - --request-audience string Request a token with an alternate audience using RFC8693 token exchange - --scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience]) - --session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml") - --skip-browser Skip opening the browser (just print the URL) + --ca-bundle strings Path to TLS certificate authority bundle (PEM format, optional, can be repeated) + --ca-bundle-data strings Base64 encoded TLS certificate authority bundle (base64 encoded PEM format, optional, can be repeated) + --client-id string OpenID Connect client ID (default "pinniped-cli") + --concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev") + --concierge-authenticator-name string Concierge authenticator name + --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') + --concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge + --concierge-endpoint string API base for the Concierge endpoint + --credential-cache string Path to cluster-specific credentials cache ("" disables the cache) (default "` + cfgDir + `/credentials.yaml") + --enable-concierge Use the Concierge to login + -h, --help help for oidc + --issuer string OpenID Connect issuer URL + --listen-port uint16 TCP port for localhost listener (authorization code flow only) + --request-audience string Request a token with an alternate audience using RFC8693 token exchange + --scopes strings OIDC scopes to request during login (default [offline_access,openid,pinniped:request-audience]) + --session-cache string Path to session cache file (default "` + cfgDir + `/sessions.yaml") + --skip-browser Skip opening the browser (just print the URL) + --upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap') (default "oidc") `), }, { @@ -134,11 +136,45 @@ func TestLoginOIDCCommand(t *testing.T) { Error: invalid Concierge parameters: invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*') `), }, + { + name: "invalid upstream type", + args: []string{ + "--issuer", "test-issuer", + "--upstream-identity-provider-type", "invalid", + }, + wantError: true, + wantStderr: here.Doc(` + Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap) + `), + }, + { + name: "oidc upstream type is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "oidc", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + wantOptionsCount: 3, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, + { + name: "ldap upstream type is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "ldap", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + wantOptionsCount: 4, + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + }, { name: "login error", args: []string{ "--client-id", "test-client-id", "--issuer", "test-issuer", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution }, loginErr: fmt.Errorf("some login error"), wantOptionsCount: 3, @@ -156,6 +192,7 @@ func TestLoginOIDCCommand(t *testing.T) { "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", "test-authenticator", "--concierge-endpoint", "https://127.0.0.1:1234/", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution }, conciergeErr: fmt.Errorf("some concierge error"), wantOptionsCount: 3, @@ -169,6 +206,7 @@ func TestLoginOIDCCommand(t *testing.T) { args: []string{ "--client-id", "test-client-id", "--issuer", "test-issuer", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution }, wantOptionsCount: 3, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", @@ -190,9 +228,11 @@ func TestLoginOIDCCommand(t *testing.T) { "--concierge-endpoint", "https://127.0.0.1:1234/", "--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()), "--concierge-api-group-suffix", "some.suffix.com", - "--credential-cache", testutil.TempDir(t) + "/credentials.yaml", + "--credential-cache", testutil.TempDir(t) + "/credentials.yaml", // must specify --credential-cache or else the cache file on disk causes test pollution + "--upstream-identity-provider-name", "some-upstream-name", + "--upstream-identity-provider-type", "ldap", }, - wantOptionsCount: 7, + wantOptionsCount: 9, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"exchanged-token"}}` + "\n", }, } diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 53b6efd4..8a0c80d6 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package oidcclient implements a CLI OIDC login flow. @@ -54,6 +54,10 @@ type handlerState struct { scopes []string cache SessionCache + upstreamIdentityProviderName string + upstreamIdentityProviderType string + ldapUpstreamIdentityProvider bool + requestedAudience string httpClient *http.Client @@ -167,6 +171,30 @@ func WithRequestAudience(audience string) Option { } } +// WithLDAPUpstreamIdentityProvider causes the login flow to use CLI prompts for username and password and causes the +// call to the Issuer's authorize endpoint to be made directly (no web browser) with the username and password on custom +// HTTP headers. This is only intended to be used when the issuer is a Pinniped Supervisor and the upstream identity +// provider is an LDAP provider. It should never be used with non-Supervisor issuers because it will send the user's +// password as a custom header, which would be ignored but could potentially get logged somewhere by the issuer. +func WithLDAPUpstreamIdentityProvider() Option { + return func(h *handlerState) error { + h.ldapUpstreamIdentityProvider = true + return nil + } +} + +// WithUpstreamIdentityProvider causes the specified name and type to be sent as custom query parameters to the +// issuer's authorize endpoint. This is only intended to be used when the issuer is a Pinniped Supervisor, in which +// case it provides a mechanism to choose among several upstream identity providers. +// Other issuers will ignore these custom query parameters. +func WithUpstreamIdentityProvider(upstreamName, upstreamType string) Option { + return func(h *handlerState) error { + h.upstreamIdentityProviderName = upstreamName + h.upstreamIdentityProviderType = upstreamType + return nil + } +} + // nopCache is a SessionCache that doesn't actually do anything. type nopCache struct{} @@ -281,6 +309,63 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { } } + // Prepare the common options for the authorization URL. We don't have the redirect URL yet though. + authorizeOptions := []oauth2.AuthCodeOption{ + oauth2.AccessTypeOffline, + h.nonce.Param(), + h.pkce.Challenge(), + h.pkce.Method(), + } + if h.upstreamIdentityProviderName != "" { + authorizeOptions = append(authorizeOptions, oauth2.SetAuthURLParam("upstream_name", h.upstreamIdentityProviderName)) + authorizeOptions = append(authorizeOptions, oauth2.SetAuthURLParam("upstream_type", h.upstreamIdentityProviderType)) + } + + // Choose the appropriate authorization and authcode exchange strategy. + var authFunc = h.webBrowserBasedAuth + if h.ldapUpstreamIdentityProvider { + authFunc = h.cliBasedAuth + } + + // Perform the authorize request and authcode exchange to get back OIDC tokens. + token, err := authFunc(&authorizeOptions) + + // If we got tokens, put them in the cache. + if err == nil { + h.cache.PutToken(cacheKey, token) + } + + return token, err +} + +// Make a direct call to the authorize endpoint and parse the authcode from the response. +// Exchange the authcode for tokens. Return the tokens or an error. +func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (*oidctypes.Token, error) { + // Make a callback URL even though we won't be listening on this port, because providing a redirect URL is + // required for OIDC authorize endpoints, and it must match the allowed redirect URL of the OIDC client + // registered on the server. + h.oauth2Config.RedirectURL = (&url.URL{ + Scheme: "http", + Host: h.listenAddr, + Path: h.callbackPath, + }).String() + + // Now that we have a redirect URL, we can build the authorize URL. + _ = h.oauth2Config.AuthCodeURL(h.state.String(), *authorizeOptions...) + + // TODO prompt for username and password + // TODO request the authorizeURL directly using h.httpClient, with the custom username and password headers + // TODO error if the response is not a 302 + // TODO error if the response Location does not include a code param (in this case it could have an error message query param to show) + // TODO check the response Location state param to see if it matches, similar to how it is done in handleAuthCodeCallback() + // TODO exchange the authcode, similar to how it is done in handleAuthCodeCallback() + // TODO return the token or any error encountered along the way + return nil, nil +} + +// Open a web browser, or ask the user to open a web browser, to visit the authorize endpoint. +// Create a localhost callback listener which exchanges the authcode for tokens. Return the tokens or an error. +func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (*oidctypes.Token, error) { // Open a TCP listener and update the OAuth2 redirect_uri to match (in case we are using an ephemeral port number). listener, err := net.Listen("tcp", h.listenAddr) if err != nil { @@ -292,18 +377,14 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { Path: h.callbackPath, }).String() + // Now that we have a redirect URL with the listener port, we can build the authorize URL. + authorizeURL := h.oauth2Config.AuthCodeURL(h.state.String(), *authorizeOptions...) + // Start a callback server in a background goroutine. shutdown := h.serve(listener) defer shutdown() // Open the authorize URL in the users browser. - authorizeURL := h.oauth2Config.AuthCodeURL( - h.state.String(), - oauth2.AccessTypeOffline, - h.nonce.Param(), - h.pkce.Challenge(), - h.pkce.Method(), - ) if err := h.openURL(authorizeURL); err != nil { return nil, fmt.Errorf("could not open browser: %w", err) } @@ -316,7 +397,6 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { if callback.err != nil { return nil, fmt.Errorf("error handling callback: %w", callback.err) } - h.cache.PutToken(cacheKey, callback.token) return callback.token, nil } } @@ -447,6 +527,8 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req // Check for error response parameters. if errorParam := params.Get("error"); errorParam != "" { + // TODO This should also show the value of the optional "error_description" param if it exists. + // See https://openid.net/specs/openid-connect-core-1_0.html#AuthError return httperr.Newf(http.StatusBadRequest, "login failed with code %q", errorParam) } From c176d15aa711c1fba4e04bf51de1dc3bc79ff3b3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 19 Apr 2021 17:59:46 -0700 Subject: [PATCH 31/59] Add Supervisor upstream LDAP login to the Pinniped CLI - Also enhance prepare-supervisor-on-kind.sh to allow setup of a working LDAP upstream IDP. --- go.mod | 1 + hack/prepare-supervisor-on-kind.sh | 113 +++++++-- internal/oidc/auth/auth_handler.go | 2 + pkg/oidcclient/login.go | 158 ++++++++++-- pkg/oidcclient/login_test.go | 390 ++++++++++++++++++++++++++++- 5 files changed, 634 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 3e09dce0..96f6e4db 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20201207232520-09787c993a3a + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d gopkg.in/square/go-jose.v2 v2.5.1 k8s.io/api v0.21.0 k8s.io/apimachinery v0.21.0 diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 5fccad0a..fea97995 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -20,6 +20,34 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" +use_oidc_upstream=no +use_ldap_upstream=no +while (("$#")); do + case "$1" in + --ldap) + use_ldap_upstream=yes + shift + ;; + --oidc) + use_oidc_upstream=yes + shift + ;; + -*) + log_error "Unsupported flag $1" >&2 + exit 1 + ;; + *) + log_error "Unsupported positional arg $1" >&2 + exit 1 + ;; + esac +done + +if [[ "$use_oidc_upstream" == "no" && "$use_ldap_upstream" == "no" ]]; then + echo "Error: Please use --oidc or --ldap to specify which type of upstream identity provider(s) you would like" + exit 1 +fi + # Read the env vars output by hack/prepare-for-integration-tests.sh source /tmp/integration-test-env @@ -73,8 +101,9 @@ sleep 5 echo "Fetching FederationDomain discovery info..." https_proxy="$PINNIPED_TEST_PROXY" curl -fLsS --cacert "$root_ca_crt_path" "$issuer/.well-known/openid-configuration" | jq . -# Make an OIDCIdentityProvider which uses Dex to provide identity. -cat <kubeconfig # Clear the local CLI cache to ensure that the kubectl command below will need to perform a fresh login. -rm -f "$HOME"/.config/pinniped/sessions.yaml +rm -f "$HOME/.config/pinniped/sessions.yaml" +rm -f "$HOME/.config/pinniped/credentials.yaml" echo echo "Ready! 🚀" -echo "To be able to access the login URL shown below, start Chrome like this:" -echo " open -a \"Google Chrome\" --args --proxy-server=\"$PINNIPED_TEST_PROXY\"" -echo "Then use these credentials at the Dex login page:" -echo " Username: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME" -echo " Password: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD" -# Perform a login using the kubectl plugin. This should print the URL to be followed for the Dex login page. +if [[ "$use_oidc_upstream" == "yes" ]]; then + echo + echo "To be able to access the login URL shown below, start Chrome like this:" + echo " open -a \"Google Chrome\" --args --proxy-server=\"$PINNIPED_TEST_PROXY\"" + echo "Then use these credentials at the Dex login page:" + echo " Username: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME" + echo " Password: $PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD" +fi + +if [[ "$use_ldap_upstream" == "yes" ]]; then + echo + echo "When prompted for username and password by the CLI, use these values:" + echo " Username: $PINNIPED_TEST_LDAP_USER_CN" + echo " Password: $PINNIPED_TEST_LDAP_USER_PASSWORD" +fi + +# Perform a login using the kubectl plugin. This should print the URL to be followed for the Dex login page +# if using an OIDC upstream, or should prompt on the CLI for username/password if using an LDAP upstream. echo echo "Running: https_proxy=\"$PINNIPED_TEST_PROXY\" no_proxy=\"127.0.0.1\" kubectl --kubeconfig ./kubeconfig get pods -A" https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" kubectl --kubeconfig ./kubeconfig get pods -A diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 434d6ce4..f5477283 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -89,6 +89,7 @@ func handleAuthRequestForLDAPUpstream( if username == "" || password == "" { // Return an error according to OIDC spec 3.1.2.6 (second paragraph). err := errors.WithStack(fosite.ErrAccessDenied.WithHintf("Missing or blank username or password.")) + plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) return nil } @@ -102,6 +103,7 @@ func handleAuthRequestForLDAPUpstream( plog.Debug("failed upstream LDAP authentication", "upstreamName", ldapUpstream.GetName()) // Return an error according to OIDC spec 3.1.2.6 (second paragraph). err = errors.WithStack(fosite.ErrAccessDenied.WithHintf("Username/password not accepted by LDAP provider.")) + plog.Info("authorize response error", oidc.FositeErrorForLog(err)...) oauthHelper.WriteAuthorizeError(w, authorizeRequester, err) return nil } diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 8a0c80d6..641aac81 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -5,13 +5,16 @@ package oidcclient import ( + "bufio" "context" "encoding/json" + "errors" "fmt" "mime" "net" "net/http" "net/url" + "os" "sort" "strings" "time" @@ -19,6 +22,7 @@ import ( "github.com/coreos/go-oidc/v3/oidc" "github.com/pkg/browser" "golang.org/x/oauth2" + "golang.org/x/term" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "go.pinniped.dev/internal/httputil/httperr" @@ -44,6 +48,16 @@ const ( // overallTimeout is the overall time that a login is allowed to take. This includes several user interactions, so // we set this to be relatively long. overallTimeout = 90 * time.Minute + + supervisorAuthorizeUpstreamNameParam = "upstream_name" + supervisorAuthorizeUpstreamTypeParam = "upstream_type" + supervisorAuthorizeUpstreamUsernameHeader = "X-Pinniped-Upstream-Username" + supervisorAuthorizeUpstreamPasswordHeader = "X-Pinniped-Upstream-Password" // nolint:gosec // this is not a credential + + defaultLDAPUsernamePrompt = "Username: " + defaultLDAPPasswordPrompt = "Password: " + + httpLocationHeaderName = "Location" ) type handlerState struct { @@ -80,6 +94,8 @@ type handlerState struct { openURL func(string) error getProvider func(*oauth2.Config, *oidc.Provider, *http.Client) provider.UpstreamOIDCIdentityProviderI validateIDToken func(ctx context.Context, provider *oidc.Provider, audience string, token string) (*oidc.IDToken, error) + promptForValue func(promptLabel string) (string, error) + promptForSecret func(promptLabel string) (string, error) callbacks chan callbackResult } @@ -103,7 +119,7 @@ func WithContext(ctx context.Context) Option { // WithListenPort specifies a TCP listen port on localhost, which will be used for the redirect_uri and to handle the // authorization code callback. By default, a random high port will be chosen which requires the authorization server -// to support wildcard port numbers as described by https://tools.ietf.org/html/rfc8252: +// to support wildcard port numbers as described by https://tools.ietf.org/html/rfc8252#section-7.3: // // The authorization server MUST allow any port to be specified at the // time of the request for loopback IP redirect URIs, to accommodate @@ -223,6 +239,8 @@ func Login(issuer string, clientID string, opts ...Option) (*oidctypes.Token, er validateIDToken: func(ctx context.Context, provider *oidc.Provider, audience string, token string) (*oidc.IDToken, error) { return provider.Verifier(&oidc.Config{ClientID: audience}).Verify(ctx, token) }, + promptForValue: promptForValue, + promptForSecret: promptForSecret, } for _, opt := range opts { if err := opt(&h); err != nil { @@ -317,8 +335,8 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { h.pkce.Method(), } if h.upstreamIdentityProviderName != "" { - authorizeOptions = append(authorizeOptions, oauth2.SetAuthURLParam("upstream_name", h.upstreamIdentityProviderName)) - authorizeOptions = append(authorizeOptions, oauth2.SetAuthURLParam("upstream_type", h.upstreamIdentityProviderType)) + authorizeOptions = append(authorizeOptions, oauth2.SetAuthURLParam(supervisorAuthorizeUpstreamNameParam, h.upstreamIdentityProviderName)) + authorizeOptions = append(authorizeOptions, oauth2.SetAuthURLParam(supervisorAuthorizeUpstreamTypeParam, h.upstreamIdentityProviderType)) } // Choose the appropriate authorization and authcode exchange strategy. @@ -341,26 +359,103 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { // Make a direct call to the authorize endpoint and parse the authcode from the response. // Exchange the authcode for tokens. Return the tokens or an error. func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (*oidctypes.Token, error) { + // Ask the user for their username and password. + username, err := h.promptForValue(defaultLDAPUsernamePrompt) + if err != nil { + return nil, fmt.Errorf("error prompting for username: %w", err) + } + password, err := h.promptForSecret(defaultLDAPPasswordPrompt) + if err != nil { + return nil, fmt.Errorf("error prompting for password: %w", err) + } + // Make a callback URL even though we won't be listening on this port, because providing a redirect URL is // required for OIDC authorize endpoints, and it must match the allowed redirect URL of the OIDC client - // registered on the server. + // registered on the server. The Supervisor oauth client does not have "localhost" in the allowed redirect + // URI list, so use 127.0.0.1. + localhostAddr := strings.ReplaceAll(h.listenAddr, "localhost", "127.0.0.1") h.oauth2Config.RedirectURL = (&url.URL{ Scheme: "http", - Host: h.listenAddr, + Host: localhostAddr, Path: h.callbackPath, }).String() // Now that we have a redirect URL, we can build the authorize URL. - _ = h.oauth2Config.AuthCodeURL(h.state.String(), *authorizeOptions...) + authorizeURL := h.oauth2Config.AuthCodeURL(h.state.String(), *authorizeOptions...) - // TODO prompt for username and password - // TODO request the authorizeURL directly using h.httpClient, with the custom username and password headers - // TODO error if the response is not a 302 - // TODO error if the response Location does not include a code param (in this case it could have an error message query param to show) - // TODO check the response Location state param to see if it matches, similar to how it is done in handleAuthCodeCallback() - // TODO exchange the authcode, similar to how it is done in handleAuthCodeCallback() - // TODO return the token or any error encountered along the way - return nil, nil + // Don't follow redirects automatically because we want to handle redirects here. + h.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // Send an authorize request. + authCtx, authorizeCtxCancelFunc := context.WithTimeout(context.Background(), httpRequestTimeout) + defer authorizeCtxCancelFunc() + authReq, err := http.NewRequestWithContext(authCtx, http.MethodGet, authorizeURL, nil) + if err != nil { + return nil, fmt.Errorf("could not build authorize request: %w", err) + } + authReq.Header.Set(supervisorAuthorizeUpstreamUsernameHeader, username) + authReq.Header.Set(supervisorAuthorizeUpstreamPasswordHeader, password) + authRes, err := h.httpClient.Do(authReq) + if err != nil { + return nil, fmt.Errorf("authorization response error: %w", err) + } + err = authRes.Body.Close() // don't need the response body + if err != nil { + return nil, fmt.Errorf("could not close authorize response body: %w", err) + } + + // A successful authorization always results in a 302. + if authRes.StatusCode != http.StatusFound { + return nil, fmt.Errorf( + "error getting authorization: expected to be redirected, but response status was %s", authRes.Status) + } + rawLocation := authRes.Header.Get(httpLocationHeaderName) + location, err := url.Parse(rawLocation) + if err != nil { + // This shouldn't be possible in practice because httpClient.Do() already parses the Location header. + return nil, fmt.Errorf("error getting authorization: could not parse redirect location: %w", err) + } + + // Check that the redirect was to the expected location. + if location.Scheme != "http" || location.Host != localhostAddr || location.Path != h.callbackPath { + return nil, fmt.Errorf("error getting authorization: redirected to the wrong location: %s", rawLocation) + } + + // Get the auth code or return the error from the server. + authCode := location.Query().Get("code") + if authCode == "" { + requiredErrorCode := location.Query().Get("error") + optionalErrorDescription := location.Query().Get("error_description") + if optionalErrorDescription == "" { + return nil, fmt.Errorf("login failed with code %q", requiredErrorCode) + } + return nil, fmt.Errorf("login failed with code %q: %s", requiredErrorCode, optionalErrorDescription) + } + + // Validate OAuth2 state and fail if it's incorrect (to block CSRF). + if err := h.state.Validate(location.Query().Get("state")); err != nil { + return nil, fmt.Errorf("missing or invalid state parameter in authorization response: %s", rawLocation) + } + + // Exchange the authorization code for access, ID, and refresh tokens and perform required + // validations on the returned ID token. + tokenCtx, tokenCtxCancelFunc := context.WithTimeout(context.Background(), httpRequestTimeout) + defer tokenCtxCancelFunc() + token, err := h.getProvider(h.oauth2Config, h.provider, h.httpClient). + ExchangeAuthcodeAndValidateTokens( + tokenCtx, + authCode, + h.pkce, + h.nonce, + h.oauth2Config.RedirectURL, + ) + if err != nil { + return nil, fmt.Errorf("error during authorization code exchange: %w", err) + } + + return token, nil } // Open a web browser, or ask the user to open a web browser, to visit the authorize endpoint. @@ -401,6 +496,41 @@ func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOp } } +func promptForValue(promptLabel string) (string, error) { + if !term.IsTerminal(0) { + return "", errors.New("stdin is not connected to a terminal") + } + _, err := fmt.Fprint(os.Stderr, promptLabel) + if err != nil { + return "", fmt.Errorf("could not print prompt to stderr: %w", err) + } + text, _ := bufio.NewReader(os.Stdin).ReadString('\n') + text = strings.ReplaceAll(text, "\n", "") + return text, nil +} + +func promptForSecret(promptLabel string) (string, error) { + if !term.IsTerminal(0) { + return "", errors.New("stdin is not connected to a terminal") + } + _, err := fmt.Fprint(os.Stderr, promptLabel) + if err != nil { + return "", fmt.Errorf("could not print prompt to stderr: %w", err) + } + password, err := term.ReadPassword(0) + if err != nil { + return "", fmt.Errorf("could not read password: %w", err) + } + // term.ReadPassword swallows the newline that was typed by the user, so to + // avoid the next line of output from happening on same line as the password + // prompt, we need to print a newline. + _, err = fmt.Fprint(os.Stderr, "\n") + if err != nil { + return "", fmt.Errorf("could not print newline to stderr: %w", err) + } + return string(password), err +} + func (h *handlerState) initOIDCDiscovery() error { // Make this method idempotent so it can be called in multiple cases with no extra network requests. if h.provider != nil { diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index 8005bcaf..e32251c4 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -6,10 +6,13 @@ package oidcclient import ( "context" "encoding/json" + "errors" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "net/url" + "strings" "testing" "time" @@ -21,6 +24,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "go.pinniped.dev/internal/httputil/httperr" + "go.pinniped.dev/internal/httputil/roundtripper" "go.pinniped.dev/internal/mocks/mockupstreamoidcidentityprovider" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/testutil" @@ -51,7 +55,7 @@ func (m *mockSessionCache) PutToken(key SessionCacheKey, token *oidctypes.Token) m.sawPutTokens = append(m.sawPutTokens, token) } -func TestLogin(t *testing.T) { +func TestLogin(t *testing.T) { // nolint:gocyclo time1 := time.Date(2035, 10, 12, 13, 14, 15, 16, time.UTC) time1Unix := int64(2075807775) require.Equal(t, time1Unix, time1.Add(2*time.Minute).Unix()) @@ -198,6 +202,51 @@ func TestLogin(t *testing.T) { require.NoError(t, json.NewEncoder(w).Encode(&response)) }) + defaultDiscoveryResponse := func(req *http.Request) (*http.Response, error) { // nolint:unparam + // Call the handler function from the test server to calculate the response. + handler, _ := providerMux.Handler(req) + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + return recorder.Result(), nil + } + + defaultLDAPTestOpts := func(t *testing.T, h *handlerState, authResponse *http.Response, authError error) error { // nolint:unparam + h.generateState = func() (state.State, error) { return "test-state", nil } + h.generatePKCE = func() (pkce.Code, error) { return "test-pkce", nil } + h.generateNonce = func() (nonce.Nonce, error) { return "test-nonce", nil } + h.promptForValue = func(promptLabel string) (string, error) { return "some-upstream-username", nil } + h.promptForSecret = func(promptLabel string) (string, error) { return "some-upstream-password", nil } + + cache := &mockSessionCache{t: t, getReturnsToken: nil} + cacheKey := SessionCacheKey{ + Issuer: successServer.URL, + ClientID: "test-client-id", + Scopes: []string{"test-scope"}, + RedirectURI: "http://localhost:0/callback", + } + t.Cleanup(func() { + require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) + }) + require.NoError(t, WithSessionCache(cache)(h)) + require.NoError(t, WithLDAPUpstreamIdentityProvider()(h)) + require.NoError(t, WithUpstreamIdentityProvider("some-upstream-name", "ldap")(h)) + + require.NoError(t, WithClient(&http.Client{ + Transport: roundtripper.Func(func(req *http.Request) (*http.Response, error) { + switch req.URL.Scheme + "://" + req.URL.Host + req.URL.Path { + case "http://" + successServer.Listener.Addr().String() + "/.well-known/openid-configuration": + return defaultDiscoveryResponse(req) + case "http://" + successServer.Listener.Addr().String() + "/authorize": + return authResponse, authError + default: + require.FailNow(t, fmt.Sprintf("saw unexpected http call from the CLI: %s", req.URL.String())) + return nil, nil + } + }), + })(h)) + return nil + } + tests := []struct { name string opt func(t *testing.T) Option @@ -512,6 +561,345 @@ func TestLogin(t *testing.T) { issuer: successServer.URL, wantToken: &testToken, }, + { + name: "upstream name and type are included in authorize request if upstream name is provided", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + h.generateState = func() (state.State, error) { return "test-state", nil } + h.generatePKCE = func() (pkce.Code, error) { return "test-pkce", nil } + h.generateNonce = func() (nonce.Nonce, error) { return "test-nonce", nil } + + cache := &mockSessionCache{t: t, getReturnsToken: nil} + cacheKey := SessionCacheKey{ + Issuer: successServer.URL, + ClientID: "test-client-id", + Scopes: []string{"test-scope"}, + RedirectURI: "http://localhost:0/callback", + } + t.Cleanup(func() { + require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) + require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawPutKeys) + require.Equal(t, []*oidctypes.Token{&testToken}, cache.sawPutTokens) + }) + require.NoError(t, WithSessionCache(cache)(h)) + require.NoError(t, WithClient(&http.Client{Timeout: 10 * time.Second})(h)) + require.NoError(t, WithUpstreamIdentityProvider("some-upstream-name", "oidc")(h)) + + h.openURL = func(actualURL string) error { + parsedActualURL, err := url.Parse(actualURL) + require.NoError(t, err) + actualParams := parsedActualURL.Query() + + require.Contains(t, actualParams.Get("redirect_uri"), "http://127.0.0.1:") + actualParams.Del("redirect_uri") + + require.Equal(t, url.Values{ + // This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example: + // $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1 + // VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g + "code_challenge": []string{"VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g"}, + "code_challenge_method": []string{"S256"}, + "response_type": []string{"code"}, + "scope": []string{"test-scope"}, + "nonce": []string{"test-nonce"}, + "state": []string{"test-state"}, + "access_type": []string{"offline"}, + "client_id": []string{"test-client-id"}, + "upstream_name": []string{"some-upstream-name"}, + "upstream_type": []string{"oidc"}, + }, actualParams) + + parsedActualURL.RawQuery = "" + require.Equal(t, successServer.URL+"/authorize", parsedActualURL.String()) + + go func() { + h.callbacks <- callbackResult{token: &testToken} + }() + return nil + } + return nil + } + }, + issuer: successServer.URL, + wantToken: &testToken, + }, + { + name: "ldap login when prompting for username returns an error", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + _ = defaultLDAPTestOpts(t, h, nil, nil) + h.promptForValue = func(promptLabel string) (string, error) { + require.Equal(t, "Username: ", promptLabel) + return "", errors.New("some prompt error") + } + return nil + } + }, + issuer: successServer.URL, + wantErr: "error prompting for username: some prompt error", + }, + { + name: "ldap login when prompting for password returns an error", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + _ = defaultLDAPTestOpts(t, h, nil, nil) + h.promptForSecret = func(promptLabel string) (string, error) { return "", errors.New("some prompt error") } + return nil + } + }, + issuer: successServer.URL, + wantErr: "error prompting for password: some prompt error", + }, + { + name: "ldap login when there is a problem with parsing the authorize URL", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + _ = defaultLDAPTestOpts(t, h, nil, nil) + require.NoError(t, WithClient(&http.Client{ + Transport: roundtripper.Func(func(req *http.Request) (*http.Response, error) { + switch req.URL.Scheme + "://" + req.URL.Host + req.URL.Path { + case "http://" + successServer.Listener.Addr().String() + "/.well-known/openid-configuration": + type providerJSON struct { + Issuer string `json:"issuer"` + AuthURL string `json:"authorization_endpoint"` + TokenURL string `json:"token_endpoint"` + JWKSURL string `json:"jwks_uri"` + } + jsonResponseBody, err := json.Marshal(&providerJSON{ + Issuer: successServer.URL, + AuthURL: "%", // this is not a legal URL! + TokenURL: successServer.URL + "/token", + JWKSURL: successServer.URL + "/keys", + }) + require.NoError(t, err) + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"content-type": []string{"application/json"}}, + Body: ioutil.NopCloser(strings.NewReader(string(jsonResponseBody))), + }, nil + default: + require.FailNow(t, fmt.Sprintf("saw unexpected http call from the CLI: %s", req.URL.String())) + return nil, nil + } + }), + })(h)) + return nil + } + }, + issuer: successServer.URL, + wantErr: `could not build authorize request: parse "%?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state&upstream_name=some-upstream-name&upstream_type=ldap": invalid URL escape "%"`, + }, + { + name: "ldap login when there is an error calling the authorization endpoint", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + return defaultLDAPTestOpts(t, h, nil, errors.New("some error fetching authorize endpoint")) + } + }, + issuer: successServer.URL, + wantErr: `authorization response error: Get "http://` + successServer.Listener.Addr().String() + + `/authorize?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state&upstream_name=some-upstream-name&upstream_type=ldap": some error fetching authorize endpoint`, + }, + { + name: "ldap login when the OIDC provider authorization endpoint returns something other than a 302 redirect", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + return defaultLDAPTestOpts(t, h, &http.Response{StatusCode: http.StatusBadGateway, Status: "502 Bad Gateway"}, nil) + } + }, + issuer: successServer.URL, + wantErr: `error getting authorization: expected to be redirected, but response status was 502 Bad Gateway`, + }, + { + name: "ldap login when the OIDC provider authorization endpoint redirect has an error and error description", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + return defaultLDAPTestOpts(t, h, &http.Response{ + StatusCode: http.StatusFound, + Header: http.Header{"Location": []string{ + "http://127.0.0.1:0/callback?error=access_denied&error_description=optional-error-description", + }}, + }, nil) + } + }, + issuer: successServer.URL, + wantErr: `login failed with code "access_denied": optional-error-description`, + }, + { + name: "ldap login when the OIDC provider authorization endpoint redirects us to a different server", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + return defaultLDAPTestOpts(t, h, &http.Response{ + StatusCode: http.StatusFound, + Header: http.Header{"Location": []string{ + "http://other-server.example.com/callback?code=foo&state=test-state", + }}, + }, nil) + } + }, + issuer: successServer.URL, + wantErr: `error getting authorization: redirected to the wrong location: http://other-server.example.com/callback?code=foo&state=test-state`, + }, + { + name: "ldap login when the OIDC provider authorization endpoint redirect has an error but no error description", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + return defaultLDAPTestOpts(t, h, &http.Response{ + StatusCode: http.StatusFound, + Header: http.Header{"Location": []string{ + "http://127.0.0.1:0/callback?error=access_denied", + }}, + }, nil) + } + }, + issuer: successServer.URL, + wantErr: `login failed with code "access_denied"`, + }, + { + name: "ldap login when the OIDC provider authorization endpoint redirect has the wrong state value", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + return defaultLDAPTestOpts(t, h, &http.Response{ + StatusCode: http.StatusFound, + Header: http.Header{"Location": []string{"http://127.0.0.1:0/callback?code=foo&state=wrong-state"}}, + }, nil) + } + }, + issuer: successServer.URL, + wantErr: `missing or invalid state parameter in authorization response: http://127.0.0.1:0/callback?code=foo&state=wrong-state`, + }, + { + name: "ldap login when there is an error exchanging the authcode or validating the tokens", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + fakeAuthCode := "test-authcode-value" + _ = defaultLDAPTestOpts(t, h, &http.Response{ + StatusCode: http.StatusFound, + Header: http.Header{"Location": []string{ + fmt.Sprintf("http://127.0.0.1:0/callback?code=%s&state=test-state", fakeAuthCode), + }}, + }, nil) + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + mock := mockUpstream(t) + mock.EXPECT(). + ExchangeAuthcodeAndValidateTokens( + gomock.Any(), fakeAuthCode, pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), "http://127.0.0.1:0/callback"). + Return(nil, errors.New("some authcode exchange or token validation error")) + return mock + } + return nil + } + }, + issuer: successServer.URL, + wantErr: "error during authorization code exchange: some authcode exchange or token validation error", + }, + { + name: "successful ldap login", + clientID: "test-client-id", + opt: func(t *testing.T) Option { + return func(h *handlerState) error { + fakeAuthCode := "test-authcode-value" + + h.getProvider = func(_ *oauth2.Config, _ *oidc.Provider, _ *http.Client) provider.UpstreamOIDCIdentityProviderI { + mock := mockUpstream(t) + mock.EXPECT(). + ExchangeAuthcodeAndValidateTokens( + gomock.Any(), fakeAuthCode, pkce.Code("test-pkce"), nonce.Nonce("test-nonce"), "http://127.0.0.1:0/callback"). + Return(&testToken, nil) + return mock + } + + h.generateState = func() (state.State, error) { return "test-state", nil } + h.generatePKCE = func() (pkce.Code, error) { return "test-pkce", nil } + h.generateNonce = func() (nonce.Nonce, error) { return "test-nonce", nil } + h.promptForValue = func(promptLabel string) (string, error) { + require.Equal(t, "Username: ", promptLabel) + return "some-upstream-username", nil + } + h.promptForSecret = func(promptLabel string) (string, error) { + require.Equal(t, "Password: ", promptLabel) + return "some-upstream-password", nil + } + + cache := &mockSessionCache{t: t, getReturnsToken: nil} + cacheKey := SessionCacheKey{ + Issuer: successServer.URL, + ClientID: "test-client-id", + Scopes: []string{"test-scope"}, + RedirectURI: "http://localhost:0/callback", + } + t.Cleanup(func() { + require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) + require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawPutKeys) + require.Equal(t, []*oidctypes.Token{&testToken}, cache.sawPutTokens) + }) + require.NoError(t, WithSessionCache(cache)(h)) + require.NoError(t, WithLDAPUpstreamIdentityProvider()(h)) + require.NoError(t, WithUpstreamIdentityProvider("some-upstream-name", "ldap")(h)) + + discoveryRequestWasMade := false + authorizeRequestWasMade := false + t.Cleanup(func() { + require.True(t, discoveryRequestWasMade, "should have made an discovery request") + require.True(t, authorizeRequestWasMade, "should have made an authorize request") + }) + + require.NoError(t, WithClient(&http.Client{ + Transport: roundtripper.Func(func(req *http.Request) (*http.Response, error) { + switch req.URL.Scheme + "://" + req.URL.Host + req.URL.Path { + case "http://" + successServer.Listener.Addr().String() + "/.well-known/openid-configuration": + discoveryRequestWasMade = true + return defaultDiscoveryResponse(req) + case "http://" + successServer.Listener.Addr().String() + "/authorize": + authorizeRequestWasMade = true + require.Equal(t, "some-upstream-username", req.Header.Get("X-Pinniped-Upstream-Username")) + require.Equal(t, "some-upstream-password", req.Header.Get("X-Pinniped-Upstream-Password")) + require.Equal(t, url.Values{ + // This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example: + // $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1 + // VVaezYqum7reIhoavCHD1n2d+piN3r/mywoYj7fCR7g + "code_challenge": []string{"VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g"}, + "code_challenge_method": []string{"S256"}, + "response_type": []string{"code"}, + "scope": []string{"test-scope"}, + "nonce": []string{"test-nonce"}, + "state": []string{"test-state"}, + "access_type": []string{"offline"}, + "client_id": []string{"test-client-id"}, + "redirect_uri": []string{"http://127.0.0.1:0/callback"}, + "upstream_name": []string{"some-upstream-name"}, + "upstream_type": []string{"ldap"}, + }, req.URL.Query()) + return &http.Response{ + StatusCode: http.StatusFound, + Header: http.Header{"Location": []string{ + fmt.Sprintf("http://127.0.0.1:0/callback?code=%s&state=test-state", fakeAuthCode), + }}, + }, nil + default: + // Note that "/token" requests should not be made. They are mocked by mocking calls to ExchangeAuthcodeAndValidateTokens(). + require.FailNow(t, fmt.Sprintf("saw unexpected http call from the CLI: %s", req.URL.String())) + return nil, nil + } + }), + })(h)) + return nil + } + }, + issuer: successServer.URL, + wantToken: &testToken, + }, { name: "with requested audience, session cache hit with valid token, but discovery fails", clientID: "test-client-id", From ddc632b99cb252074bbbb23c95d59fe208fea3be Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 19 Apr 2021 18:08:52 -0700 Subject: [PATCH 32/59] Show the error_description when it is included in authorization response --- pkg/oidcclient/login.go | 18 ++++++++++-------- pkg/oidcclient/login_test.go | 10 ++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 641aac81..f79b192f 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -423,9 +423,15 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) ( return nil, fmt.Errorf("error getting authorization: redirected to the wrong location: %s", rawLocation) } + // Validate OAuth2 state and fail if it's incorrect (to block CSRF). + if err := h.state.Validate(location.Query().Get("state")); err != nil { + return nil, fmt.Errorf("missing or invalid state parameter in authorization response: %s", rawLocation) + } + // Get the auth code or return the error from the server. authCode := location.Query().Get("code") if authCode == "" { + // Check for error response parameters. See https://openid.net/specs/openid-connect-core-1_0.html#AuthError. requiredErrorCode := location.Query().Get("error") optionalErrorDescription := location.Query().Get("error_description") if optionalErrorDescription == "" { @@ -434,11 +440,6 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) ( return nil, fmt.Errorf("login failed with code %q: %s", requiredErrorCode, optionalErrorDescription) } - // Validate OAuth2 state and fail if it's incorrect (to block CSRF). - if err := h.state.Validate(location.Query().Get("state")); err != nil { - return nil, fmt.Errorf("missing or invalid state parameter in authorization response: %s", rawLocation) - } - // Exchange the authorization code for access, ID, and refresh tokens and perform required // validations on the returned ID token. tokenCtx, tokenCtxCancelFunc := context.WithTimeout(context.Background(), httpRequestTimeout) @@ -655,10 +656,11 @@ func (h *handlerState) handleAuthCodeCallback(w http.ResponseWriter, r *http.Req return httperr.New(http.StatusForbidden, "missing or invalid state parameter") } - // Check for error response parameters. + // Check for error response parameters. See https://openid.net/specs/openid-connect-core-1_0.html#AuthError. if errorParam := params.Get("error"); errorParam != "" { - // TODO This should also show the value of the optional "error_description" param if it exists. - // See https://openid.net/specs/openid-connect-core-1_0.html#AuthError + if errorDescParam := params.Get("error_description"); errorDescParam != "" { + return httperr.Newf(http.StatusBadRequest, "login failed with code %q: %s", errorParam, errorDescParam) + } return httperr.Newf(http.StatusBadRequest, "login failed with code %q", errorParam) } diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index e32251c4..eb9b5147 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -724,7 +724,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo return defaultLDAPTestOpts(t, h, &http.Response{ StatusCode: http.StatusFound, Header: http.Header{"Location": []string{ - "http://127.0.0.1:0/callback?error=access_denied&error_description=optional-error-description", + "http://127.0.0.1:0/callback?error=access_denied&error_description=optional-error-description&state=test-state", }}, }, nil) } @@ -756,7 +756,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo return defaultLDAPTestOpts(t, h, &http.Response{ StatusCode: http.StatusFound, Header: http.Header{"Location": []string{ - "http://127.0.0.1:0/callback?error=access_denied", + "http://127.0.0.1:0/callback?error=access_denied&state=test-state", }}, }, nil) } @@ -1279,6 +1279,12 @@ func TestHandleAuthCodeCallback(t *testing.T) { wantErr: `login failed with code "some_error"`, wantHTTPStatus: http.StatusBadRequest, }, + { + name: "error code with a description from provider", + query: "state=test-state&error=some_error&error_description=optional%20error%20description", + wantErr: `login failed with code "some_error": optional error description`, + wantHTTPStatus: http.StatusBadRequest, + }, { name: "invalid code", query: "state=test-state&code=invalid", From 6a350aa4e1b30f73c69ff7af9a12d3be3a54eb39 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 22 Apr 2021 16:58:48 -0700 Subject: [PATCH 33/59] Fix some LDAP CA bundle handling - Make PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE optional for integration tests - When there is no CA bundle provided, be careful to use nil instead of an empty bundle, because nil means to use the OS defaults --- internal/upstreamldap/upstreamldap.go | 3 ++- test/library/env.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index ae296db5..5848ebd0 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -124,8 +124,9 @@ func (p *Provider) dial(ctx context.Context) (Conn, error) { // Unfortunately, the go-ldap library does not seem to support dialing with a context.Context, // so we implement it ourselves, heavily inspired by ldap.DialURL. func (p *Provider) dialTLS(ctx context.Context, hostAndPort string) (Conn, error) { - rootCAs := x509.NewCertPool() + var rootCAs *x509.CertPool if p.c.CABundle != nil { + rootCAs = x509.NewCertPool() if !rootCAs.AppendCertsFromPEM(p.c.CABundle) { return nil, ldap.NewError(ldap.ErrorNetwork, fmt.Errorf("could not parse CA bundle")) } diff --git a/test/library/env.go b/test/library/env.go index 78aeaec5..0e730d05 100644 --- a/test/library/env.go +++ b/test/library/env.go @@ -236,7 +236,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { result.SupervisorUpstreamLDAP = TestLDAPUpstream{ Host: needEnv(t, "PINNIPED_TEST_LDAP_HOST"), - CABundle: base64Decoded(t, needEnv(t, "PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE")), + CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE")), BindUsername: needEnv(t, "PINNIPED_TEST_LDAP_BIND_ACCOUNT_USERNAME"), BindPassword: needEnv(t, "PINNIPED_TEST_LDAP_BIND_ACCOUNT_PASSWORD"), UserSearchBase: needEnv(t, "PINNIPED_TEST_LDAP_USERS_SEARCH_BASE"), From 9b818dbf10b8f52fe0175613e8be020d0fb24085 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 22 Apr 2021 16:59:42 -0700 Subject: [PATCH 34/59] Remove another 10s sleep related to JWTAuthenticator initialization --- hack/prepare-supervisor-on-kind.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index fea97995..baf9c541 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -194,8 +194,7 @@ spec: EOF echo "Waiting for JWTAuthenticator to initialize..." -# Our integration tests wait 10 seconds, so use that same value here. -sleep 10 +sleep 5 # Compile the CLI. go build ./cmd/pinniped From 263a33cc85e4351ecc00e96b6d7713dd1d51bbfe Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 27 Apr 2021 12:43:09 -0700 Subject: [PATCH 35/59] Some updates based on PR review --- .../types_ldapidentityprovider.go.tmpl | 28 +++----- cmd/pinniped-supervisor/main.go | 2 - ...or.pinniped.dev_ldapidentityproviders.yaml | 28 ++++---- generated/1.17/README.adoc | 72 ++++++++----------- .../v1alpha1/types_ldapidentityprovider.go | 28 +++----- .../idp/v1alpha1/zz_generated.deepcopy.go | 66 +++++++---------- ...or.pinniped.dev_ldapidentityproviders.yaml | 28 ++++---- generated/1.18/README.adoc | 72 ++++++++----------- .../v1alpha1/types_ldapidentityprovider.go | 28 +++----- .../idp/v1alpha1/zz_generated.deepcopy.go | 66 +++++++---------- ...or.pinniped.dev_ldapidentityproviders.yaml | 28 ++++---- generated/1.19/README.adoc | 72 ++++++++----------- .../v1alpha1/types_ldapidentityprovider.go | 28 +++----- .../idp/v1alpha1/zz_generated.deepcopy.go | 66 +++++++---------- ...or.pinniped.dev_ldapidentityproviders.yaml | 28 ++++---- generated/1.20/README.adoc | 72 ++++++++----------- .../v1alpha1/types_ldapidentityprovider.go | 28 +++----- .../idp/v1alpha1/zz_generated.deepcopy.go | 66 +++++++---------- ...or.pinniped.dev_ldapidentityproviders.yaml | 28 ++++---- .../v1alpha1/types_ldapidentityprovider.go | 28 +++----- .../idp/v1alpha1/zz_generated.deepcopy.go | 66 +++++++---------- hack/prepare-supervisor-on-kind.sh | 2 +- .../upstreamwatcher/ldap_upstream_watcher.go | 13 +++- .../ldap_upstream_watcher_test.go | 16 ++--- internal/oidc/auth/auth_handler.go | 4 +- internal/oidc/auth/auth_handler_test.go | 4 +- .../testutil/oidctestutil/oidctestutil.go | 2 +- internal/upstreamldap/upstreamldap.go | 9 ++- internal/upstreamldap/upstreamldap_test.go | 8 ++- pkg/oidcclient/login.go | 24 +++---- pkg/oidcclient/login_test.go | 16 ++--- test/integration/e2e_test.go | 4 +- test/integration/supervisor_login_test.go | 24 +++---- 33 files changed, 441 insertions(+), 613 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index c7a6b747..0699ddfc 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -35,42 +35,36 @@ type LDAPIdentityProviderStatus struct { Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } -type LDAPIdentityProviderTLSSpec struct { - // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. - // If omitted, a default set of system roots will be trusted. - // +optional - CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` -} - -type LDAPIdentityProviderBindSpec struct { +type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // The password must be non-empty. // +kubebuilder:validation:MinLength=1 SecretName string `json:"secretName"` } -type LDAPIdentityProviderUserSearchAttributesSpec struct { +type LDAPIdentityProviderUserSearchAttributes struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field - // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` - // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 - UniqueID string `json:"uniqueID,omitempty"` + UID string `json:"uid,omitempty"` } -type LDAPIdentityProviderUserSearchSpec struct { +type LDAPIdentityProviderUserSearch struct { // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` @@ -88,7 +82,7 @@ type LDAPIdentityProviderUserSearchSpec struct { // Attributes specifies how the user's information should be read from the LDAP entry which was found as // the result of the user search. // +optional - Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` + Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } // Spec for configuring an LDAP identity provider. @@ -98,14 +92,14 @@ type LDAPIdentityProviderSpec struct { Host string `json:"host"` // TLS contains the connection settings for how to establish the connection to the Host. - TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + TLS *TLSSpec `json:"tls,omitempty"` // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. - Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + Bind LDAPIdentityProviderBind `json:"bind,omitempty"` // UserSearch contains the configuration for searching for a user by name in the LDAP provider. - UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 9acf404f..7d196333 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -245,8 +245,6 @@ func startControllers( WithController( upstreamwatcher.NewLDAPUpstreamWatcherController( dynamicUpstreamIDPProvider, - // nil means to use a real production dialer when creating objects to add to the dynamicUpstreamIDPProvider cache. - nil, pinnipedClient, pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), secretInformer, diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index c9924a16..5ed9901b 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -64,6 +64,7 @@ spec: The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + The password must be non-empty. minLength: 1 type: string required: @@ -111,9 +112,8 @@ spec: the connection to the Host. properties: certificateAuthorityData: - description: X.509 Certificate Authority (base64-encoded PEM bundle) - to trust when connecting to the LDAP provider. If omitted, a - default set of system roots will be trusted. + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. type: string type: object userSearch: @@ -125,15 +125,14 @@ spec: be read from the LDAP entry which was found as the result of the user search. properties: - uniqueID: - description: UniqueID specifies the name of the attribute - in the LDAP entry which whose value shall be used to uniquely - identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". The value - of this field is case-sensitive and must match the case - of the attribute name returned by the LDAP server in the - user's entry. Distinguished names can be used by specifying - lower-case "dn". + uid: + description: UID specifies the name of the attribute in the + LDAP entry which whose value shall be used to uniquely identify + the user within this LDAP provider after a successful authentication. + E.g. "uidNumber" or "objectGUID". The value of this field + is case-sensitive and must match the case of the attribute + name returned by the LDAP server in the user's entry. Distinguished + names can be used by specifying lower-case "dn". minLength: 1 type: string username: @@ -146,9 +145,8 @@ spec: the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearchSpec's Filter field - cannot be blank, since the default value of "dn={}" would - not work. + then the LDAPIdentityProviderUserSearch's Filter field cannot + be blank, since the default value of "dn={}" would not work. minLength: 1 type: string type: object diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 27df8cb7..111a374a 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -721,8 +721,8 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec"] -==== LDAPIdentityProviderBindSpec +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind"] +==== LDAPIdentityProviderBind @@ -734,7 +734,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. |=== @@ -754,9 +754,9 @@ Spec for configuring an LDAP identity provider. |=== | Field | Description | *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. -| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. -| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. | *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== @@ -779,43 +779,8 @@ Status of an LDAP identity provider. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec"] -==== LDAPIdentityProviderTLSSpec - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] -**** - -[cols="25a,75a", options="header"] -|=== -| Field | Description -| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. If omitted, a default set of system roots will be trusted. -|=== - - -[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec"] -==== LDAPIdentityProviderUserSearchAttributesSpec - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$] -**** - -[cols="25a,75a", options="header"] -|=== -| Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". -|=== - - -[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec"] -==== LDAPIdentityProviderUserSearchSpec +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch"] +==== LDAPIdentityProviderUserSearch @@ -829,7 +794,25 @@ Status of an LDAP identity provider. | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". | *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. -| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes"] +==== LDAPIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -953,6 +936,7 @@ Status of an OIDC identity provider. .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index c7a6b747..0699ddfc 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -35,42 +35,36 @@ type LDAPIdentityProviderStatus struct { Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } -type LDAPIdentityProviderTLSSpec struct { - // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. - // If omitted, a default set of system roots will be trusted. - // +optional - CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` -} - -type LDAPIdentityProviderBindSpec struct { +type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // The password must be non-empty. // +kubebuilder:validation:MinLength=1 SecretName string `json:"secretName"` } -type LDAPIdentityProviderUserSearchAttributesSpec struct { +type LDAPIdentityProviderUserSearchAttributes struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field - // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` - // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 - UniqueID string `json:"uniqueID,omitempty"` + UID string `json:"uid,omitempty"` } -type LDAPIdentityProviderUserSearchSpec struct { +type LDAPIdentityProviderUserSearch struct { // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` @@ -88,7 +82,7 @@ type LDAPIdentityProviderUserSearchSpec struct { // Attributes specifies how the user's information should be read from the LDAP entry which was found as // the result of the user search. // +optional - Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` + Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } // Spec for configuring an LDAP identity provider. @@ -98,14 +92,14 @@ type LDAPIdentityProviderSpec struct { Host string `json:"host"` // TLS contains the connection settings for how to establish the connection to the Host. - TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + TLS *TLSSpec `json:"tls,omitempty"` // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. - Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + Bind LDAPIdentityProviderBind `json:"bind,omitempty"` // UserSearch contains the configuration for searching for a user by name in the LDAP provider. - UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 1b19762c..c48c570f 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -57,17 +57,17 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { +func (in *LDAPIdentityProviderBind) DeepCopyInto(out *LDAPIdentityProviderBind) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. -func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBind. +func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { if in == nil { return nil } - out := new(LDAPIdentityProviderBindSpec) + out := new(LDAPIdentityProviderBind) in.DeepCopyInto(out) return out } @@ -110,7 +110,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) *out = *in if in.TLS != nil { in, out := &in.TLS, &out.TLS - *out = new(LDAPIdentityProviderTLSSpec) + *out = new(TLSSpec) **out = **in } out.Bind = in.Bind @@ -152,50 +152,34 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. -func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderTLSSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderUserSearchAttributesSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { +func (in *LDAPIdentityProviderUserSearch) DeepCopyInto(out *LDAPIdentityProviderUserSearch) { *out = *in out.Attributes = in.Attributes return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearch. +func (in *LDAPIdentityProviderUserSearch) DeepCopy() *LDAPIdentityProviderUserSearch { if in == nil { return nil } - out := new(LDAPIdentityProviderUserSearchSpec) + out := new(LDAPIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributes. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopy() *LDAPIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributes) in.DeepCopyInto(out) return out } diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index c9924a16..5ed9901b 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -64,6 +64,7 @@ spec: The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + The password must be non-empty. minLength: 1 type: string required: @@ -111,9 +112,8 @@ spec: the connection to the Host. properties: certificateAuthorityData: - description: X.509 Certificate Authority (base64-encoded PEM bundle) - to trust when connecting to the LDAP provider. If omitted, a - default set of system roots will be trusted. + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. type: string type: object userSearch: @@ -125,15 +125,14 @@ spec: be read from the LDAP entry which was found as the result of the user search. properties: - uniqueID: - description: UniqueID specifies the name of the attribute - in the LDAP entry which whose value shall be used to uniquely - identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". The value - of this field is case-sensitive and must match the case - of the attribute name returned by the LDAP server in the - user's entry. Distinguished names can be used by specifying - lower-case "dn". + uid: + description: UID specifies the name of the attribute in the + LDAP entry which whose value shall be used to uniquely identify + the user within this LDAP provider after a successful authentication. + E.g. "uidNumber" or "objectGUID". The value of this field + is case-sensitive and must match the case of the attribute + name returned by the LDAP server in the user's entry. Distinguished + names can be used by specifying lower-case "dn". minLength: 1 type: string username: @@ -146,9 +145,8 @@ spec: the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearchSpec's Filter field - cannot be blank, since the default value of "dn={}" would - not work. + then the LDAPIdentityProviderUserSearch's Filter field cannot + be blank, since the default value of "dn={}" would not work. minLength: 1 type: string type: object diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 2671cc14..f4849bba 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -721,8 +721,8 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec"] -==== LDAPIdentityProviderBindSpec +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind"] +==== LDAPIdentityProviderBind @@ -734,7 +734,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. |=== @@ -754,9 +754,9 @@ Spec for configuring an LDAP identity provider. |=== | Field | Description | *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. -| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. -| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. | *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== @@ -779,43 +779,8 @@ Status of an LDAP identity provider. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec"] -==== LDAPIdentityProviderTLSSpec - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] -**** - -[cols="25a,75a", options="header"] -|=== -| Field | Description -| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. If omitted, a default set of system roots will be trusted. -|=== - - -[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec"] -==== LDAPIdentityProviderUserSearchAttributesSpec - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$] -**** - -[cols="25a,75a", options="header"] -|=== -| Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". -|=== - - -[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec"] -==== LDAPIdentityProviderUserSearchSpec +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch"] +==== LDAPIdentityProviderUserSearch @@ -829,7 +794,25 @@ Status of an LDAP identity provider. | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". | *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. -| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes"] +==== LDAPIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -953,6 +936,7 @@ Status of an OIDC identity provider. .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index c7a6b747..0699ddfc 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -35,42 +35,36 @@ type LDAPIdentityProviderStatus struct { Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } -type LDAPIdentityProviderTLSSpec struct { - // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. - // If omitted, a default set of system roots will be trusted. - // +optional - CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` -} - -type LDAPIdentityProviderBindSpec struct { +type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // The password must be non-empty. // +kubebuilder:validation:MinLength=1 SecretName string `json:"secretName"` } -type LDAPIdentityProviderUserSearchAttributesSpec struct { +type LDAPIdentityProviderUserSearchAttributes struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field - // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` - // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 - UniqueID string `json:"uniqueID,omitempty"` + UID string `json:"uid,omitempty"` } -type LDAPIdentityProviderUserSearchSpec struct { +type LDAPIdentityProviderUserSearch struct { // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` @@ -88,7 +82,7 @@ type LDAPIdentityProviderUserSearchSpec struct { // Attributes specifies how the user's information should be read from the LDAP entry which was found as // the result of the user search. // +optional - Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` + Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } // Spec for configuring an LDAP identity provider. @@ -98,14 +92,14 @@ type LDAPIdentityProviderSpec struct { Host string `json:"host"` // TLS contains the connection settings for how to establish the connection to the Host. - TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + TLS *TLSSpec `json:"tls,omitempty"` // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. - Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + Bind LDAPIdentityProviderBind `json:"bind,omitempty"` // UserSearch contains the configuration for searching for a user by name in the LDAP provider. - UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 1b19762c..c48c570f 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -57,17 +57,17 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { +func (in *LDAPIdentityProviderBind) DeepCopyInto(out *LDAPIdentityProviderBind) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. -func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBind. +func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { if in == nil { return nil } - out := new(LDAPIdentityProviderBindSpec) + out := new(LDAPIdentityProviderBind) in.DeepCopyInto(out) return out } @@ -110,7 +110,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) *out = *in if in.TLS != nil { in, out := &in.TLS, &out.TLS - *out = new(LDAPIdentityProviderTLSSpec) + *out = new(TLSSpec) **out = **in } out.Bind = in.Bind @@ -152,50 +152,34 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. -func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderTLSSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderUserSearchAttributesSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { +func (in *LDAPIdentityProviderUserSearch) DeepCopyInto(out *LDAPIdentityProviderUserSearch) { *out = *in out.Attributes = in.Attributes return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearch. +func (in *LDAPIdentityProviderUserSearch) DeepCopy() *LDAPIdentityProviderUserSearch { if in == nil { return nil } - out := new(LDAPIdentityProviderUserSearchSpec) + out := new(LDAPIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributes. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopy() *LDAPIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributes) in.DeepCopyInto(out) return out } diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index c9924a16..5ed9901b 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -64,6 +64,7 @@ spec: The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + The password must be non-empty. minLength: 1 type: string required: @@ -111,9 +112,8 @@ spec: the connection to the Host. properties: certificateAuthorityData: - description: X.509 Certificate Authority (base64-encoded PEM bundle) - to trust when connecting to the LDAP provider. If omitted, a - default set of system roots will be trusted. + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. type: string type: object userSearch: @@ -125,15 +125,14 @@ spec: be read from the LDAP entry which was found as the result of the user search. properties: - uniqueID: - description: UniqueID specifies the name of the attribute - in the LDAP entry which whose value shall be used to uniquely - identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". The value - of this field is case-sensitive and must match the case - of the attribute name returned by the LDAP server in the - user's entry. Distinguished names can be used by specifying - lower-case "dn". + uid: + description: UID specifies the name of the attribute in the + LDAP entry which whose value shall be used to uniquely identify + the user within this LDAP provider after a successful authentication. + E.g. "uidNumber" or "objectGUID". The value of this field + is case-sensitive and must match the case of the attribute + name returned by the LDAP server in the user's entry. Distinguished + names can be used by specifying lower-case "dn". minLength: 1 type: string username: @@ -146,9 +145,8 @@ spec: the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearchSpec's Filter field - cannot be blank, since the default value of "dn={}" would - not work. + then the LDAPIdentityProviderUserSearch's Filter field cannot + be blank, since the default value of "dn={}" would not work. minLength: 1 type: string type: object diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 00302c8c..0a938587 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -721,8 +721,8 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec"] -==== LDAPIdentityProviderBindSpec +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind"] +==== LDAPIdentityProviderBind @@ -734,7 +734,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. |=== @@ -754,9 +754,9 @@ Spec for configuring an LDAP identity provider. |=== | Field | Description | *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. -| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. -| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. | *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== @@ -779,43 +779,8 @@ Status of an LDAP identity provider. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec"] -==== LDAPIdentityProviderTLSSpec - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] -**** - -[cols="25a,75a", options="header"] -|=== -| Field | Description -| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. If omitted, a default set of system roots will be trusted. -|=== - - -[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec"] -==== LDAPIdentityProviderUserSearchAttributesSpec - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$] -**** - -[cols="25a,75a", options="header"] -|=== -| Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". -|=== - - -[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec"] -==== LDAPIdentityProviderUserSearchSpec +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch"] +==== LDAPIdentityProviderUserSearch @@ -829,7 +794,25 @@ Status of an LDAP identity provider. | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". | *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. -| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes"] +==== LDAPIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -953,6 +936,7 @@ Status of an OIDC identity provider. .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index c7a6b747..0699ddfc 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -35,42 +35,36 @@ type LDAPIdentityProviderStatus struct { Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } -type LDAPIdentityProviderTLSSpec struct { - // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. - // If omitted, a default set of system roots will be trusted. - // +optional - CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` -} - -type LDAPIdentityProviderBindSpec struct { +type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // The password must be non-empty. // +kubebuilder:validation:MinLength=1 SecretName string `json:"secretName"` } -type LDAPIdentityProviderUserSearchAttributesSpec struct { +type LDAPIdentityProviderUserSearchAttributes struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field - // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` - // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 - UniqueID string `json:"uniqueID,omitempty"` + UID string `json:"uid,omitempty"` } -type LDAPIdentityProviderUserSearchSpec struct { +type LDAPIdentityProviderUserSearch struct { // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` @@ -88,7 +82,7 @@ type LDAPIdentityProviderUserSearchSpec struct { // Attributes specifies how the user's information should be read from the LDAP entry which was found as // the result of the user search. // +optional - Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` + Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } // Spec for configuring an LDAP identity provider. @@ -98,14 +92,14 @@ type LDAPIdentityProviderSpec struct { Host string `json:"host"` // TLS contains the connection settings for how to establish the connection to the Host. - TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + TLS *TLSSpec `json:"tls,omitempty"` // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. - Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + Bind LDAPIdentityProviderBind `json:"bind,omitempty"` // UserSearch contains the configuration for searching for a user by name in the LDAP provider. - UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 1b19762c..c48c570f 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -57,17 +57,17 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { +func (in *LDAPIdentityProviderBind) DeepCopyInto(out *LDAPIdentityProviderBind) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. -func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBind. +func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { if in == nil { return nil } - out := new(LDAPIdentityProviderBindSpec) + out := new(LDAPIdentityProviderBind) in.DeepCopyInto(out) return out } @@ -110,7 +110,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) *out = *in if in.TLS != nil { in, out := &in.TLS, &out.TLS - *out = new(LDAPIdentityProviderTLSSpec) + *out = new(TLSSpec) **out = **in } out.Bind = in.Bind @@ -152,50 +152,34 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. -func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderTLSSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderUserSearchAttributesSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { +func (in *LDAPIdentityProviderUserSearch) DeepCopyInto(out *LDAPIdentityProviderUserSearch) { *out = *in out.Attributes = in.Attributes return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearch. +func (in *LDAPIdentityProviderUserSearch) DeepCopy() *LDAPIdentityProviderUserSearch { if in == nil { return nil } - out := new(LDAPIdentityProviderUserSearchSpec) + out := new(LDAPIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributes. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopy() *LDAPIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributes) in.DeepCopyInto(out) return out } diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index c9924a16..5ed9901b 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -64,6 +64,7 @@ spec: The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + The password must be non-empty. minLength: 1 type: string required: @@ -111,9 +112,8 @@ spec: the connection to the Host. properties: certificateAuthorityData: - description: X.509 Certificate Authority (base64-encoded PEM bundle) - to trust when connecting to the LDAP provider. If omitted, a - default set of system roots will be trusted. + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. type: string type: object userSearch: @@ -125,15 +125,14 @@ spec: be read from the LDAP entry which was found as the result of the user search. properties: - uniqueID: - description: UniqueID specifies the name of the attribute - in the LDAP entry which whose value shall be used to uniquely - identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". The value - of this field is case-sensitive and must match the case - of the attribute name returned by the LDAP server in the - user's entry. Distinguished names can be used by specifying - lower-case "dn". + uid: + description: UID specifies the name of the attribute in the + LDAP entry which whose value shall be used to uniquely identify + the user within this LDAP provider after a successful authentication. + E.g. "uidNumber" or "objectGUID". The value of this field + is case-sensitive and must match the case of the attribute + name returned by the LDAP server in the user's entry. Distinguished + names can be used by specifying lower-case "dn". minLength: 1 type: string username: @@ -146,9 +145,8 @@ spec: the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearchSpec's Filter field - cannot be blank, since the default value of "dn={}" would - not work. + then the LDAPIdentityProviderUserSearch's Filter field cannot + be blank, since the default value of "dn={}" would not work. minLength: 1 type: string type: object diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index fe6e5796..805fe2bc 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -721,8 +721,8 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec"] -==== LDAPIdentityProviderBindSpec +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind"] +==== LDAPIdentityProviderBind @@ -734,7 +734,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. |=== @@ -754,9 +754,9 @@ Spec for configuring an LDAP identity provider. |=== | Field | Description | *`host`* __string__ | Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. -| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec[$$LDAPIdentityProviderTLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. -| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbindspec[$$LDAPIdentityProviderBindSpec$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. -| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. +| *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. +| *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. | *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== @@ -779,43 +779,8 @@ Status of an LDAP identity provider. |=== -[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidertlsspec"] -==== LDAPIdentityProviderTLSSpec - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] -**** - -[cols="25a,75a", options="header"] -|=== -| Field | Description -| *`certificateAuthorityData`* __string__ | X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. If omitted, a default set of system roots will be trusted. -|=== - - -[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec"] -==== LDAPIdentityProviderUserSearchAttributesSpec - - - -.Appears In: -**** -- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec[$$LDAPIdentityProviderUserSearchSpec$$] -**** - -[cols="25a,75a", options="header"] -|=== -| Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". -|=== - - -[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchspec"] -==== LDAPIdentityProviderUserSearchSpec +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch"] +==== LDAPIdentityProviderUserSearch @@ -829,7 +794,25 @@ Status of an LDAP identity provider. | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". | *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. -| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributes"] +==== LDAPIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -953,6 +936,7 @@ Status of an OIDC identity provider. .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] - xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-oidcidentityproviderspec[$$OIDCIdentityProviderSpec$$] **** diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index c7a6b747..0699ddfc 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -35,42 +35,36 @@ type LDAPIdentityProviderStatus struct { Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } -type LDAPIdentityProviderTLSSpec struct { - // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. - // If omitted, a default set of system roots will be trusted. - // +optional - CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` -} - -type LDAPIdentityProviderBindSpec struct { +type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // The password must be non-empty. // +kubebuilder:validation:MinLength=1 SecretName string `json:"secretName"` } -type LDAPIdentityProviderUserSearchAttributesSpec struct { +type LDAPIdentityProviderUserSearchAttributes struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field - // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` - // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 - UniqueID string `json:"uniqueID,omitempty"` + UID string `json:"uid,omitempty"` } -type LDAPIdentityProviderUserSearchSpec struct { +type LDAPIdentityProviderUserSearch struct { // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` @@ -88,7 +82,7 @@ type LDAPIdentityProviderUserSearchSpec struct { // Attributes specifies how the user's information should be read from the LDAP entry which was found as // the result of the user search. // +optional - Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` + Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } // Spec for configuring an LDAP identity provider. @@ -98,14 +92,14 @@ type LDAPIdentityProviderSpec struct { Host string `json:"host"` // TLS contains the connection settings for how to establish the connection to the Host. - TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + TLS *TLSSpec `json:"tls,omitempty"` // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. - Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + Bind LDAPIdentityProviderBind `json:"bind,omitempty"` // UserSearch contains the configuration for searching for a user by name in the LDAP provider. - UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 1b19762c..c48c570f 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -57,17 +57,17 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { +func (in *LDAPIdentityProviderBind) DeepCopyInto(out *LDAPIdentityProviderBind) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. -func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBind. +func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { if in == nil { return nil } - out := new(LDAPIdentityProviderBindSpec) + out := new(LDAPIdentityProviderBind) in.DeepCopyInto(out) return out } @@ -110,7 +110,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) *out = *in if in.TLS != nil { in, out := &in.TLS, &out.TLS - *out = new(LDAPIdentityProviderTLSSpec) + *out = new(TLSSpec) **out = **in } out.Bind = in.Bind @@ -152,50 +152,34 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. -func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderTLSSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderUserSearchAttributesSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { +func (in *LDAPIdentityProviderUserSearch) DeepCopyInto(out *LDAPIdentityProviderUserSearch) { *out = *in out.Attributes = in.Attributes return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearch. +func (in *LDAPIdentityProviderUserSearch) DeepCopy() *LDAPIdentityProviderUserSearch { if in == nil { return nil } - out := new(LDAPIdentityProviderUserSearchSpec) + out := new(LDAPIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributes. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopy() *LDAPIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributes) in.DeepCopyInto(out) return out } diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index c9924a16..5ed9901b 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -64,6 +64,7 @@ spec: The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + The password must be non-empty. minLength: 1 type: string required: @@ -111,9 +112,8 @@ spec: the connection to the Host. properties: certificateAuthorityData: - description: X.509 Certificate Authority (base64-encoded PEM bundle) - to trust when connecting to the LDAP provider. If omitted, a - default set of system roots will be trusted. + description: X.509 Certificate Authority (base64-encoded PEM bundle). + If omitted, a default set of system roots will be trusted. type: string type: object userSearch: @@ -125,15 +125,14 @@ spec: be read from the LDAP entry which was found as the result of the user search. properties: - uniqueID: - description: UniqueID specifies the name of the attribute - in the LDAP entry which whose value shall be used to uniquely - identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". The value - of this field is case-sensitive and must match the case - of the attribute name returned by the LDAP server in the - user's entry. Distinguished names can be used by specifying - lower-case "dn". + uid: + description: UID specifies the name of the attribute in the + LDAP entry which whose value shall be used to uniquely identify + the user within this LDAP provider after a successful authentication. + E.g. "uidNumber" or "objectGUID". The value of this field + is case-sensitive and must match the case of the attribute + name returned by the LDAP server in the user's entry. Distinguished + names can be used by specifying lower-case "dn". minLength: 1 type: string username: @@ -146,9 +145,8 @@ spec: the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearchSpec's Filter field - cannot be blank, since the default value of "dn={}" would - not work. + then the LDAPIdentityProviderUserSearch's Filter field cannot + be blank, since the default value of "dn={}" would not work. minLength: 1 type: string type: object diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index c7a6b747..0699ddfc 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -35,42 +35,36 @@ type LDAPIdentityProviderStatus struct { Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } -type LDAPIdentityProviderTLSSpec struct { - // X.509 Certificate Authority (base64-encoded PEM bundle) to trust when connecting to the LDAP provider. - // If omitted, a default set of system roots will be trusted. - // +optional - CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` -} - -type LDAPIdentityProviderBindSpec struct { +type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // The password must be non-empty. // +kubebuilder:validation:MinLength=1 SecretName string `json:"secretName"` } -type LDAPIdentityProviderUserSearchAttributesSpec struct { +type LDAPIdentityProviderUserSearchAttributes struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field - // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` - // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely + // UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 - UniqueID string `json:"uniqueID,omitempty"` + UID string `json:"uid,omitempty"` } -type LDAPIdentityProviderUserSearchSpec struct { +type LDAPIdentityProviderUserSearch struct { // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` @@ -88,7 +82,7 @@ type LDAPIdentityProviderUserSearchSpec struct { // Attributes specifies how the user's information should be read from the LDAP entry which was found as // the result of the user search. // +optional - Attributes LDAPIdentityProviderUserSearchAttributesSpec `json:"attributes,omitempty"` + Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } // Spec for configuring an LDAP identity provider. @@ -98,14 +92,14 @@ type LDAPIdentityProviderSpec struct { Host string `json:"host"` // TLS contains the connection settings for how to establish the connection to the Host. - TLS *LDAPIdentityProviderTLSSpec `json:"tls,omitempty"` + TLS *TLSSpec `json:"tls,omitempty"` // Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. - Bind LDAPIdentityProviderBindSpec `json:"bind,omitempty"` + Bind LDAPIdentityProviderBind `json:"bind,omitempty"` // UserSearch contains the configuration for searching for a user by name in the LDAP provider. - UserSearch LDAPIdentityProviderUserSearchSpec `json:"userSearch,omitempty"` + UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index 1b19762c..c48c570f 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -57,17 +57,17 @@ func (in *LDAPIdentityProvider) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderBindSpec) DeepCopyInto(out *LDAPIdentityProviderBindSpec) { +func (in *LDAPIdentityProviderBind) DeepCopyInto(out *LDAPIdentityProviderBind) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBindSpec. -func (in *LDAPIdentityProviderBindSpec) DeepCopy() *LDAPIdentityProviderBindSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderBind. +func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { if in == nil { return nil } - out := new(LDAPIdentityProviderBindSpec) + out := new(LDAPIdentityProviderBind) in.DeepCopyInto(out) return out } @@ -110,7 +110,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) *out = *in if in.TLS != nil { in, out := &in.TLS, &out.TLS - *out = new(LDAPIdentityProviderTLSSpec) + *out = new(TLSSpec) **out = **in } out.Bind = in.Bind @@ -152,50 +152,34 @@ func (in *LDAPIdentityProviderStatus) DeepCopy() *LDAPIdentityProviderStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderTLSSpec) DeepCopyInto(out *LDAPIdentityProviderTLSSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderTLSSpec. -func (in *LDAPIdentityProviderTLSSpec) DeepCopy() *LDAPIdentityProviderTLSSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderTLSSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributesSpec) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributesSpec. -func (in *LDAPIdentityProviderUserSearchAttributesSpec) DeepCopy() *LDAPIdentityProviderUserSearchAttributesSpec { - if in == nil { - return nil - } - out := new(LDAPIdentityProviderUserSearchAttributesSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopyInto(out *LDAPIdentityProviderUserSearchSpec) { +func (in *LDAPIdentityProviderUserSearch) DeepCopyInto(out *LDAPIdentityProviderUserSearch) { *out = *in out.Attributes = in.Attributes return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchSpec. -func (in *LDAPIdentityProviderUserSearchSpec) DeepCopy() *LDAPIdentityProviderUserSearchSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearch. +func (in *LDAPIdentityProviderUserSearch) DeepCopy() *LDAPIdentityProviderUserSearch { if in == nil { return nil } - out := new(LDAPIdentityProviderUserSearchSpec) + out := new(LDAPIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderUserSearchAttributes. +func (in *LDAPIdentityProviderUserSearchAttributes) DeepCopy() *LDAPIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderUserSearchAttributes) in.DeepCopyInto(out) return out } diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index baf9c541..a5009c66 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -156,7 +156,7 @@ spec: base: "$PINNIPED_TEST_LDAP_USERS_SEARCH_BASE" filter: "cn={}" attributes: - uniqueID: "$PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_NAME" + uid: "$PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_NAME" username: "$PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME" dryRunAuthenticationUsername: "$PINNIPED_TEST_LDAP_USER_CN" EOF diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index dd0bd73f..df7406b0 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -61,6 +61,17 @@ type ldapWatcherController struct { // NewLDAPUpstreamWatcherController instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. func NewLDAPUpstreamWatcherController( + idpCache UpstreamLDAPIdentityProviderICache, + client pinnipedclientset.Interface, + ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, + secretInformer corev1informers.SecretInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, +) controllerlib.Controller { + // nil means to use a real production dialer when creating objects to add to the dynamicUpstreamIDPProvider cache. + return newInternal(idpCache, nil, client, ldapIdentityProviderInformer, secretInformer, withInformer) +} + +func newInternal( idpCache UpstreamLDAPIdentityProviderICache, ldapDialer upstreamldap.LDAPDialer, client pinnipedclientset.Interface, @@ -124,7 +135,7 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * Base: spec.UserSearch.Base, Filter: spec.UserSearch.Filter, UsernameAttribute: spec.UserSearch.Attributes.Username, - UIDAttribute: spec.UserSearch.Attributes.UniqueID, + UIDAttribute: spec.UserSearch.Attributes.UID, }, Dialer: c.ldapDialer, } diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index e904f2a4..79049037 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -80,7 +80,7 @@ func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { secretInformer := kubeInformers.Core().V1().Secrets() withInformer := testutil.NewObservableWithInformerOption() - NewLDAPUpstreamWatcherController(nil, nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) + NewLDAPUpstreamWatcherController(nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) unrelated := corev1.Secret{} filter := withInformer.GetFilterForInformer(secretInformer) @@ -125,7 +125,7 @@ func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) secretInformer := kubeInformers.Core().V1().Secrets() withInformer := testutil.NewObservableWithInformerOption() - NewLDAPUpstreamWatcherController(nil, nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) + NewLDAPUpstreamWatcherController(nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) unrelated := corev1.Secret{} filter := withInformer.GetFilterForInformer(ldapIDPInformer) @@ -174,14 +174,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, Spec: v1alpha1.LDAPIdentityProviderSpec{ Host: testHost, - TLS: &v1alpha1.LDAPIdentityProviderTLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, - Bind: v1alpha1.LDAPIdentityProviderBindSpec{SecretName: testSecretName}, - UserSearch: v1alpha1.LDAPIdentityProviderUserSearchSpec{ + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, + Bind: v1alpha1.LDAPIdentityProviderBind{SecretName: testSecretName}, + UserSearch: v1alpha1.LDAPIdentityProviderUserSearch{ Base: testUserSearchBase, Filter: testUserSearchFilter, - Attributes: v1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ + Attributes: v1alpha1.LDAPIdentityProviderUserSearchAttributes{ Username: testUsernameAttrName, - UniqueID: testUIDAttrName, + UID: testUIDAttrName, }, }, }, @@ -815,7 +815,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { return conn, nil })} - controller := NewLDAPUpstreamWatcherController( + controller := newInternal( cache, dialer, fakePinnipedClient, diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index f5477283..12b0e915 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -27,8 +27,8 @@ import ( ) const ( - CustomUsernameHeaderName = "X-Pinniped-Upstream-Username" - CustomPasswordHeaderName = "X-Pinniped-Upstream-Password" //nolint:gosec // this is not a credential + CustomUsernameHeaderName = "X-Pinniped-Idp-Username" + CustomPasswordHeaderName = "X-Pinniped-Idp-Password" //nolint:gosec // this is not a credential ) func NewHandler( diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 2a597881..efb164e3 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -1119,10 +1119,10 @@ func TestAuthorizationEndpoint(t *testing.T) { req.Header.Set("Cookie", test.csrfCookie) } if test.customUsernameHeader != nil { - req.Header.Set("X-Pinniped-Upstream-Username", *test.customUsernameHeader) + req.Header.Set("X-Pinniped-Idp-Username", *test.customUsernameHeader) } if test.customPasswordHeader != nil { - req.Header.Set("X-Pinniped-Upstream-Password", *test.customPasswordHeader) + req.Header.Set("X-Pinniped-Idp-Password", *test.customPasswordHeader) } rsp := httptest.NewRecorder() subject.ServeHTTP(rsp, req) diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index fdf998d1..e4718270 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -39,7 +39,7 @@ import ( // Test helpers for the OIDC package. -// ExchangeAuthcodeAndValidateTokenArgs is a POGO (plain old go object?) used to spy on calls to +// ExchangeAuthcodeAndValidateTokenArgs is used to spy on calls to // TestUpstreamOIDCIdentityProvider.ExchangeAuthcodeAndValidateTokensFunc(). type ExchangeAuthcodeAndValidateTokenArgs struct { Ctx context.Context diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 5848ebd0..ef7c6d0c 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -8,6 +8,7 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "fmt" "net" "strings" @@ -23,7 +24,6 @@ const ( ldapsScheme = "ldaps" distinguishedNameAttributeName = "dn" userSearchFilterInterpolationLocationMarker = "{}" - invalidCredentialsErrorPrefix = `LDAP Result Code 49 "Invalid Credentials":` ) // Conn abstracts the upstream LDAP communication protocol (mostly for testing). @@ -46,6 +46,8 @@ type LDAPDialer interface { // LDAPDialerFunc makes it easy to use a func as an LDAPDialer. type LDAPDialerFunc func(ctx context.Context, hostAndPort string) (Conn, error) +var _ LDAPDialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) { return nil, nil }) + func (f LDAPDialerFunc) Dial(ctx context.Context, hostAndPort string) (Conn, error) { return f(ctx, hostAndPort) } @@ -307,7 +309,8 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c if err != nil { plog.DebugErr("error binding for user (if this is not the expected dn for this username, please check the user search configuration)", err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN) - if strings.HasPrefix(err.Error(), invalidCredentialsErrorPrefix) { + ldapErr := &ldap.Error{} + if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials { return "", "", nil } return "", "", fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) @@ -321,7 +324,7 @@ func (p *Provider) userSearchRequest(username string) *ldap.SearchRequest { return &ldap.SearchRequest{ BaseDN: p.c.UserSearch.Base, Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.DerefAlways, // TODO what's the best value here? + DerefAliases: ldap.NeverDerefAliases, SizeLimit: 2, TimeLimit: 90, TypesOnly: false, diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 73a42e08..b4e64cce 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -67,7 +67,7 @@ func TestEndUserAuthentication(t *testing.T) { request := &ldap.SearchRequest{ BaseDN: testUserSearchBase, Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.DerefAlways, + DerefAliases: ldap.NeverDerefAliases, SizeLimit: 2, TimeLimit: 90, TypesOnly: false, @@ -571,7 +571,11 @@ func TestEndUserAuthentication(t *testing.T) { wantUnauthenticated: true, skipDryRunAuthenticateUser: true, bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(errors.New(`LDAP Result Code 49 "Invalid Credentials": some bind error`)).Times(1) + err := &ldap.Error{ + Err: errors.New("some bind error"), + ResultCode: ldap.LDAPResultInvalidCredentials, + } + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(err).Times(1) }, }, { diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index f79b192f..289ef31b 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -49,10 +49,10 @@ const ( // we set this to be relatively long. overallTimeout = 90 * time.Minute - supervisorAuthorizeUpstreamNameParam = "upstream_name" - supervisorAuthorizeUpstreamTypeParam = "upstream_type" - supervisorAuthorizeUpstreamUsernameHeader = "X-Pinniped-Upstream-Username" - supervisorAuthorizeUpstreamPasswordHeader = "X-Pinniped-Upstream-Password" // nolint:gosec // this is not a credential + supervisorAuthorizeUpstreamNameParam = "pinniped_idp_name" + supervisorAuthorizeUpstreamTypeParam = "pinniped_idp_type" + supervisorAuthorizeUpstreamUsernameHeader = "X-Pinniped-Idp-Username" + supervisorAuthorizeUpstreamPasswordHeader = "X-Pinniped-Idp-Password" // nolint:gosec // this is not a credential defaultLDAPUsernamePrompt = "Username: " defaultLDAPPasswordPrompt = "Password: " @@ -401,10 +401,7 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) ( if err != nil { return nil, fmt.Errorf("authorization response error: %w", err) } - err = authRes.Body.Close() // don't need the response body - if err != nil { - return nil, fmt.Errorf("could not close authorize response body: %w", err) - } + _ = authRes.Body.Close() // don't need the response body, and okay if it fails to close // A successful authorization always results in a 302. if authRes.StatusCode != http.StatusFound { @@ -498,20 +495,23 @@ func (h *handlerState) webBrowserBasedAuth(authorizeOptions *[]oauth2.AuthCodeOp } func promptForValue(promptLabel string) (string, error) { - if !term.IsTerminal(0) { + if !term.IsTerminal(int(os.Stdin.Fd())) { return "", errors.New("stdin is not connected to a terminal") } _, err := fmt.Fprint(os.Stderr, promptLabel) if err != nil { return "", fmt.Errorf("could not print prompt to stderr: %w", err) } - text, _ := bufio.NewReader(os.Stdin).ReadString('\n') - text = strings.ReplaceAll(text, "\n", "") + text, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + return "", fmt.Errorf("could read input from stdin: %w", err) + } + text = strings.TrimSpace(text) return text, nil } func promptForSecret(promptLabel string) (string, error) { - if !term.IsTerminal(0) { + if !term.IsTerminal(int(os.Stdin.Fd())) { return "", errors.New("stdin is not connected to a terminal") } _, err := fmt.Fprint(os.Stderr, promptLabel) diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index eb9b5147..d20a10e1 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -606,8 +606,8 @@ func TestLogin(t *testing.T) { // nolint:gocyclo "state": []string{"test-state"}, "access_type": []string{"offline"}, "client_id": []string{"test-client-id"}, - "upstream_name": []string{"some-upstream-name"}, - "upstream_type": []string{"oidc"}, + "pinniped_idp_name": []string{"some-upstream-name"}, + "pinniped_idp_type": []string{"oidc"}, }, actualParams) parsedActualURL.RawQuery = "" @@ -691,7 +691,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo } }, issuer: successServer.URL, - wantErr: `could not build authorize request: parse "%?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state&upstream_name=some-upstream-name&upstream_type=ldap": invalid URL escape "%"`, + wantErr: `could not build authorize request: parse "%?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&pinniped_idp_name=some-upstream-name&pinniped_idp_type=ldap&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state": invalid URL escape "%"`, }, { name: "ldap login when there is an error calling the authorization endpoint", @@ -703,7 +703,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo }, issuer: successServer.URL, wantErr: `authorization response error: Get "http://` + successServer.Listener.Addr().String() + - `/authorize?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state&upstream_name=some-upstream-name&upstream_type=ldap": some error fetching authorize endpoint`, + `/authorize?access_type=offline&client_id=test-client-id&code_challenge=VVaezYqum7reIhoavCHD1n2d-piN3r_mywoYj7fCR7g&code_challenge_method=S256&nonce=test-nonce&pinniped_idp_name=some-upstream-name&pinniped_idp_type=ldap&redirect_uri=http%3A%2F%2F127.0.0.1%3A0%2Fcallback&response_type=code&scope=test-scope&state=test-state": some error fetching authorize endpoint`, }, { name: "ldap login when the OIDC provider authorization endpoint returns something other than a 302 redirect", @@ -863,8 +863,8 @@ func TestLogin(t *testing.T) { // nolint:gocyclo return defaultDiscoveryResponse(req) case "http://" + successServer.Listener.Addr().String() + "/authorize": authorizeRequestWasMade = true - require.Equal(t, "some-upstream-username", req.Header.Get("X-Pinniped-Upstream-Username")) - require.Equal(t, "some-upstream-password", req.Header.Get("X-Pinniped-Upstream-Password")) + require.Equal(t, "some-upstream-username", req.Header.Get("X-Pinniped-Idp-Username")) + require.Equal(t, "some-upstream-password", req.Header.Get("X-Pinniped-Idp-Password")) require.Equal(t, url.Values{ // This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example: // $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1 @@ -878,8 +878,8 @@ func TestLogin(t *testing.T) { // nolint:gocyclo "access_type": []string{"offline"}, "client_id": []string{"test-client-id"}, "redirect_uri": []string{"http://127.0.0.1:0/callback"}, - "upstream_name": []string{"some-upstream-name"}, - "upstream_type": []string{"ldap"}, + "pinniped_idp_name": []string{"some-upstream-name"}, + "pinniped_idp_type": []string{"ldap"}, }, req.URL.Query()) return &http.Response{ StatusCode: http.StatusFound, diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 38f22b2b..7c357711 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -327,6 +327,8 @@ status: `, string(kubectlOutput3)) + expectedGroupsPlusUnauthenticated := append([]string{}, env.SupervisorUpstreamOIDC.ExpectedGroups...) + expectedGroupsPlusUnauthenticated = append(expectedGroupsPlusUnauthenticated, "system:authenticated") // Validate that `pinniped whoami` returns the correct identity. assertWhoami( ctx, @@ -335,6 +337,6 @@ status: pinnipedExe, kubeconfigPath, env.SupervisorUpstreamOIDC.Username, - append(env.SupervisorUpstreamOIDC.ExpectedGroups, "system:authenticated"), + expectedGroupsPlusUnauthenticated, ) } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index ba47ed0d..d9b2fd83 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -78,18 +78,18 @@ func TestSupervisorLogin(t *testing.T) { ) ldapIDP := library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.LDAPIdentityProviderTLSSpec{ + TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), }, - Bind: idpv1alpha1.LDAPIdentityProviderBindSpec{ + Bind: idpv1alpha1.LDAPIdentityProviderBind{ SecretName: secret.Name, }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearchSpec{ + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ Base: env.SupervisorUpstreamLDAP.UserSearchBase, Filter: "", - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, - UniqueID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, DryRunAuthenticationUsername: env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, @@ -129,18 +129,18 @@ func TestSupervisorLogin(t *testing.T) { ) ldapIDP := library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ Host: env.SupervisorUpstreamLDAP.Host, - TLS: &idpv1alpha1.LDAPIdentityProviderTLSSpec{ + TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), }, - Bind: idpv1alpha1.LDAPIdentityProviderBindSpec{ + Bind: idpv1alpha1.LDAPIdentityProviderBind{ SecretName: secret.Name, }, - UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearchSpec{ + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ Base: env.SupervisorUpstreamLDAP.UserSearchBase, Filter: "cn={}", // try using a non-default search filter - Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{ + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ Username: "dn", // try using the user's DN as the downstream username - UniqueID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, DryRunAuthenticationUsername: "", // try without dry run @@ -467,8 +467,8 @@ func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAutho require.NoError(t, err) // Set the custom username/password headers for the LDAP authorize request. - authRequest.Header.Set("X-Pinniped-Upstream-Username", upstreamUsername) - authRequest.Header.Set("X-Pinniped-Upstream-Password", upstreamPassword) + authRequest.Header.Set("X-Pinniped-Idp-Username", upstreamUsername) + authRequest.Header.Set("X-Pinniped-Idp-Password", upstreamPassword) authResponse, err := httpClient.Do(authRequest) require.NoError(t, err) From 5c62a9d0bdddb1f50fc941b5a2af42cacfbcbf1c Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 27 Apr 2021 16:54:26 -0700 Subject: [PATCH 36/59] More adjustments based on PR feedback --- ...{fosite_sotrage_interface.go => fosite_storage_interface.go} | 0 internal/upstreamldap/upstreamldap.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename internal/fositestoragei/{fosite_sotrage_interface.go => fosite_storage_interface.go} (100%) diff --git a/internal/fositestoragei/fosite_sotrage_interface.go b/internal/fositestoragei/fosite_storage_interface.go similarity index 100% rename from internal/fositestoragei/fosite_sotrage_interface.go rename to internal/fositestoragei/fosite_storage_interface.go diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index ef7c6d0c..f163f59d 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -46,7 +46,7 @@ type LDAPDialer interface { // LDAPDialerFunc makes it easy to use a func as an LDAPDialer. type LDAPDialerFunc func(ctx context.Context, hostAndPort string) (Conn, error) -var _ LDAPDialer = LDAPDialerFunc(func(ctx context.Context, hostAndPort string) (Conn, error) { return nil, nil }) +var _ LDAPDialer = LDAPDialerFunc(nil) func (f LDAPDialerFunc) Dial(ctx context.Context, hostAndPort string) (Conn, error) { return f(ctx, hostAndPort) From 4bd83add354fee8800894018d6c646d3c7679c69 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 28 Apr 2021 13:14:21 -0700 Subject: [PATCH 37/59] Add Supervisor upstream IDP discovery on the server-side --- internal/oidc/discovery/discovery_handler.go | 74 ++++++++++++---- .../oidc/discovery/discovery_handler_test.go | 86 +++++++++++++++++-- internal/oidc/provider/manager/manager.go | 2 +- .../oidc/provider/manager/manager_test.go | 23 +++-- test/integration/supervisor_discovery_test.go | 3 +- 5 files changed, 149 insertions(+), 39 deletions(-) diff --git a/internal/oidc/discovery/discovery_handler.go b/internal/oidc/discovery/discovery_handler.go index b45c1042..dabdda02 100644 --- a/internal/oidc/discovery/discovery_handler.go +++ b/internal/oidc/discovery/discovery_handler.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package discovery provides a handler for the OIDC discovery endpoint. @@ -8,10 +8,16 @@ import ( "bytes" "encoding/json" "net/http" + "sort" "go.pinniped.dev/internal/oidc" ) +const ( + idpDiscoveryTypeLDAP = "ldap" + idpDiscoveryTypeOIDC = "oidc" +) + // Metadata holds all fields (that we care about) from the OpenID Provider Metadata section in the // OpenID Connect Discovery specification: // https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3. @@ -37,33 +43,28 @@ type Metadata struct { ClaimsSupported []string `json:"claims_supported"` // ^^^ Optional ^^^ + + // vvv Custom vvv + + IDPs []IdentityProviderMetadata `json:"pinniped_idps"` + + // ^^^ Custom ^^^ +} + +type IdentityProviderMetadata struct { + Name string `json:"name"` + Type string `json:"type"` } // NewHandler returns an http.Handler that serves an OIDC discovery endpoint. -func NewHandler(issuerURL string) http.Handler { - oidcConfig := Metadata{ - Issuer: issuerURL, - AuthorizationEndpoint: issuerURL + oidc.AuthorizationEndpointPath, - TokenEndpoint: issuerURL + oidc.TokenEndpointPath, - JWKSURI: issuerURL + oidc.JWKSEndpointPath, - ResponseTypesSupported: []string{"code"}, - SubjectTypesSupported: []string{"public"}, - IDTokenSigningAlgValuesSupported: []string{"ES256"}, - TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, - ScopesSupported: []string{"openid", "offline"}, - ClaimsSupported: []string{"groups"}, - } - - var b bytes.Buffer - encodeErr := json.NewEncoder(&b).Encode(&oidcConfig) - encodedMetadata := b.Bytes() - +func NewHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, `Method not allowed (try GET)`, http.StatusMethodNotAllowed) return } + encodedMetadata, encodeErr := metadata(issuerURL, upstreamIDPs) if encodeErr != nil { http.Error(w, encodeErr.Error(), http.StatusInternalServerError) return @@ -76,3 +77,38 @@ func NewHandler(issuerURL string) http.Handler { } }) } + +func metadata(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister) ([]byte, error) { + oidcConfig := Metadata{ + Issuer: issuerURL, + AuthorizationEndpoint: issuerURL + oidc.AuthorizationEndpointPath, + TokenEndpoint: issuerURL + oidc.TokenEndpointPath, + JWKSURI: issuerURL + oidc.JWKSEndpointPath, + ResponseTypesSupported: []string{"code"}, + SubjectTypesSupported: []string{"public"}, + IDTokenSigningAlgValuesSupported: []string{"ES256"}, + TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, + ScopesSupported: []string{"openid", "offline"}, + ClaimsSupported: []string{"groups"}, + IDPs: []IdentityProviderMetadata{}, + } + + // The cache of IDPs could change at any time, so always recalculate the list. + for _, provider := range upstreamIDPs.GetLDAPIdentityProviders() { + oidcConfig.IDPs = append(oidcConfig.IDPs, IdentityProviderMetadata{Name: provider.GetName(), Type: idpDiscoveryTypeLDAP}) + } + for _, provider := range upstreamIDPs.GetOIDCIdentityProviders() { + oidcConfig.IDPs = append(oidcConfig.IDPs, IdentityProviderMetadata{Name: provider.GetName(), Type: idpDiscoveryTypeOIDC}) + } + + // Nobody like an API that changes the results unnecessarily. :) + sort.SliceStable(oidcConfig.IDPs, func(i, j int) bool { + return oidcConfig.IDPs[i].Name < oidcConfig.IDPs[j].Name + }) + + var b bytes.Buffer + encodeErr := json.NewEncoder(&b).Encode(&oidcConfig) + encodedMetadata := b.Bytes() + + return encodedMetadata, encodeErr +} diff --git a/internal/oidc/discovery/discovery_handler_test.go b/internal/oidc/discovery/discovery_handler_test.go index f15c9a0c..7236e544 100644 --- a/internal/oidc/discovery/discovery_handler_test.go +++ b/internal/oidc/discovery/discovery_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package discovery @@ -9,6 +9,10 @@ import ( "net/http/httptest" "testing" + "go.pinniped.dev/internal/oidc/provider" + + "go.pinniped.dev/internal/testutil/oidctestutil" + "github.com/stretchr/testify/require" "go.pinniped.dev/internal/oidc" @@ -22,10 +26,11 @@ func TestDiscovery(t *testing.T) { method string path string - wantStatus int - wantContentType string - wantBodyJSON interface{} - wantBodyString string + wantStatus int + wantContentType string + wantFirstResponseBodyJSON interface{} + wantSecondResponseBodyJSON interface{} + wantBodyString string }{ { name: "happy path", @@ -34,7 +39,7 @@ func TestDiscovery(t *testing.T) { path: "/some/path" + oidc.WellKnownEndpointPath, wantStatus: http.StatusOK, wantContentType: "application/json", - wantBodyJSON: &Metadata{ + wantFirstResponseBodyJSON: &Metadata{ Issuer: "https://some-issuer.com/some/path", AuthorizationEndpoint: "https://some-issuer.com/some/path/oauth2/authorize", TokenEndpoint: "https://some-issuer.com/some/path/oauth2/token", @@ -45,6 +50,32 @@ func TestDiscovery(t *testing.T) { TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, ScopesSupported: []string{"openid", "offline"}, ClaimsSupported: []string{"groups"}, + IDPs: []IdentityProviderMetadata{ + {Name: "a-some-ldap-idp", Type: "ldap"}, + {Name: "a-some-oidc-idp", Type: "oidc"}, + {Name: "x-some-idp", Type: "ldap"}, + {Name: "x-some-idp", Type: "oidc"}, + {Name: "z-some-ldap-idp", Type: "ldap"}, + {Name: "z-some-oidc-idp", Type: "oidc"}, + }, + }, + wantSecondResponseBodyJSON: &Metadata{ + Issuer: "https://some-issuer.com/some/path", + AuthorizationEndpoint: "https://some-issuer.com/some/path/oauth2/authorize", + TokenEndpoint: "https://some-issuer.com/some/path/oauth2/token", + JWKSURI: "https://some-issuer.com/some/path/jwks.json", + ResponseTypesSupported: []string{"code"}, + SubjectTypesSupported: []string{"public"}, + IDTokenSigningAlgValuesSupported: []string{"ES256"}, + TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, + ScopesSupported: []string{"openid", "offline"}, + ClaimsSupported: []string{"groups"}, + IDPs: []IdentityProviderMetadata{ + {Name: "some-other-ldap-idp-1", Type: "ldap"}, + {Name: "some-other-ldap-idp-2", Type: "ldap"}, + {Name: "some-other-oidc-idp-1", Type: "oidc"}, + {Name: "some-other-oidc-idp-2", Type: "oidc"}, + }, }, }, { @@ -60,7 +91,16 @@ func TestDiscovery(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - handler := NewHandler(test.issuer) + idpLister := oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "z-some-oidc-idp"}). + WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "x-some-idp"}). + WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "a-some-ldap-idp"}). + WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "a-some-oidc-idp"}). + WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ldap-idp"}). + WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "x-some-idp"}). + Build() + + handler := NewHandler(test.issuer, idpLister) req := httptest.NewRequest(test.method, test.path, nil) rsp := httptest.NewRecorder() handler.ServeHTTP(rsp, req) @@ -69,8 +109,36 @@ func TestDiscovery(t *testing.T) { require.Equal(t, test.wantContentType, rsp.Header().Get("Content-Type")) - if test.wantBodyJSON != nil { - wantJSON, err := json.Marshal(test.wantBodyJSON) + if test.wantFirstResponseBodyJSON != nil { + wantJSON, err := json.Marshal(test.wantFirstResponseBodyJSON) + require.NoError(t, err) + require.JSONEq(t, string(wantJSON), rsp.Body.String()) + } + + if test.wantBodyString != "" { + require.Equal(t, test.wantBodyString, rsp.Body.String()) + } + + // Change the list of IDPs in the cache. + idpLister.SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-1"}, + &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-2"}, + }) + idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ + &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-1"}, + &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-2"}, + }) + + // Make the same request to the same handler instance again, and expect different results. + rsp = httptest.NewRecorder() + handler.ServeHTTP(rsp, req) + + require.Equal(t, test.wantStatus, rsp.Code) + + require.Equal(t, test.wantContentType, rsp.Header().Get("Content-Type")) + + if test.wantFirstResponseBodyJSON != nil { + wantJSON, err := json.Marshal(test.wantSecondResponseBodyJSON) require.NoError(t, err) require.JSONEq(t, string(wantJSON), rsp.Body.String()) } diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index f1192edb..e17a4737 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -102,7 +102,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderBlockKey), ) - m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer) + m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer, m.upstreamIDPs) m.providerHandlers[(issuerHostWithPath + oidc.JWKSEndpointPath)] = jwks.NewHandler(issuer, m.dynamicJWKSProvider) diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index 04f30fa0..f42fe11c 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -52,6 +52,8 @@ func TestManager(t *testing.T) { issuer2DifferentCaseHostname = "https://exAmPlE.Com/some/path/more/deeply/nested/path" issuer2KeyID = "issuer2-key" upstreamIDPAuthorizationURL = "https://test-upstream.com/auth" + upstreamIDPName = "test-idp" + upstreamIDPType = "oidc" downstreamClientID = "pinniped-cli" downstreamRedirectURL = "http://127.0.0.1:12345/callback" @@ -68,7 +70,7 @@ func TestManager(t *testing.T) { return req } - requireDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIssuerInResponse string) { + requireDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIssuer, expectedIDPName, expectedIDPType string) { recorder := httptest.NewRecorder() subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.WellKnownEndpointPath+requestURLSuffix)) @@ -82,7 +84,10 @@ func TestManager(t *testing.T) { parsedDiscoveryResult := discovery.Metadata{} err = json.Unmarshal(responseBody, &parsedDiscoveryResult) r.NoError(err) - r.Equal(expectedIssuerInResponse, parsedDiscoveryResult.Issuer) + r.Equal(expectedIssuer, parsedDiscoveryResult.Issuer) + r.Len(parsedDiscoveryResult.IDPs, 1) + r.Equal(expectedIDPName, parsedDiscoveryResult.IDPs[0].Name) + r.Equal(expectedIDPType, parsedDiscoveryResult.IDPs[0].Type) } requireAuthorizationRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedRedirectLocationPrefix string) (string, string) { @@ -222,7 +227,7 @@ func TestManager(t *testing.T) { parsedUpstreamIDPAuthorizationURL, err := url.Parse(upstreamIDPAuthorizationURL) r.NoError(err) idpLister := oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{ - Name: "test-idp", + Name: upstreamIDPName, ClientID: "test-client-id", AuthorizationURL: *parsedUpstreamIDPAuthorizationURL, Scopes: []string{"test-scope"}, @@ -284,14 +289,14 @@ func TestManager(t *testing.T) { } requireRoutesMatchingRequestsToAppropriateProvider := func() { - requireDiscoveryRequestToBeHandled(issuer1, "", issuer1) - requireDiscoveryRequestToBeHandled(issuer2, "", issuer2) - requireDiscoveryRequestToBeHandled(issuer2, "?some=query", issuer2) + requireDiscoveryRequestToBeHandled(issuer1, "", issuer1, upstreamIDPName, upstreamIDPType) + requireDiscoveryRequestToBeHandled(issuer2, "", issuer2, upstreamIDPName, upstreamIDPType) + requireDiscoveryRequestToBeHandled(issuer2, "?some=query", issuer2, upstreamIDPName, upstreamIDPType) // Hostnames are case-insensitive, so test that we can handle that. - requireDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", issuer1) - requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", issuer2) - requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2) + requireDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", issuer1, upstreamIDPName, upstreamIDPType) + requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", issuer2, upstreamIDPName, upstreamIDPType) + requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2, upstreamIDPName, upstreamIDPType) issuer1JWKS := requireJWKSRequestToBeHandled(issuer1, "", issuer1KeyID) issuer2JWKS := requireJWKSRequestToBeHandled(issuer2, "", issuer2KeyID) diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 2bcfd03d..07b58de0 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -483,7 +483,8 @@ func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, superviso "response_types_supported": ["code"], "claims_supported": ["groups"], "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": ["ES256"] + "id_token_signing_alg_values_supported": ["ES256"], + "pinniped_idps": [] }`) expectedJSON := fmt.Sprintf(expectedResultTemplate, issuerName, issuerName, issuerName, issuerName) From 36819989a30f8694cb2e70692898e845906d1f84 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 28 Apr 2021 14:26:57 -0700 Subject: [PATCH 38/59] Remove DryRunAuthenticationUsername from LDAPIdentityProviderSpec Signed-off-by: Margo Crawford --- .../types_ldapidentityprovider.go.tmpl | 22 ---- ...or.pinniped.dev_ldapidentityproviders.yaml | 32 ----- generated/1.17/README.adoc | 1 - .../v1alpha1/types_ldapidentityprovider.go | 22 ---- ...or.pinniped.dev_ldapidentityproviders.yaml | 32 ----- generated/1.18/README.adoc | 1 - .../v1alpha1/types_ldapidentityprovider.go | 22 ---- ...or.pinniped.dev_ldapidentityproviders.yaml | 32 ----- generated/1.19/README.adoc | 1 - .../v1alpha1/types_ldapidentityprovider.go | 22 ---- ...or.pinniped.dev_ldapidentityproviders.yaml | 32 ----- generated/1.20/README.adoc | 1 - .../v1alpha1/types_ldapidentityprovider.go | 22 ---- ...or.pinniped.dev_ldapidentityproviders.yaml | 32 ----- .../v1alpha1/types_ldapidentityprovider.go | 22 ---- hack/prepare-supervisor-on-kind.sh | 1 - .../upstreamwatcher/ldap_upstream_watcher.go | 58 +-------- .../ldap_upstream_watcher_test.go | 119 ------------------ test/integration/supervisor_login_test.go | 11 +- 19 files changed, 10 insertions(+), 475 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index 0699ddfc..d718ba65 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -100,28 +100,6 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` - - // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. - // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection - // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success - // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. - // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a - // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username - // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for - // that end user during the authentication. This will test all of the configuration options of the - // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the - // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships - // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate - // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". - // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able - // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch - // configuration were set up such that an end user should log in using their email address as their username, then - // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP - // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your - // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration - // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become - // disabled in the future. - DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 5ed9901b..d396129d 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -70,38 +70,6 @@ spec: required: - secretName type: object - dryRunAuthenticationUsername: - description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's - configuration is validated. When DryRunAuthenticationUsername is - blank, the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server using the Host and TLS settings and also will - bind using the Bind settings. The success or failure of the connect - and bind will be reflected in the LDAPIdentityProvider's status - conditions array. When DryRunAuthenticationUsername is not blank, - the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server and performing a full dry run of authenticating - as the end user with the username specified by DryRunAuthenticationUsername. - The dry run will act as if the correct password were specified for - that end user during the authentication. This will test all of the - configuration options of the LDAPIdentityProvider. The success or - failure of the authentication dry run will be reflected in the LDAPIdentityProvider's - status conditions array, along with details of what username, UID, - and group memberships were selected for the specified user. If the - dry run fails, then that user would not be able to authenticate - in a real authentication situation either, so the LDAPIdentityProvider's - Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername - must be a valid username of a real user who should be able to authenticate - given all of the LDAPIdentityProvider's configuration. For example, - if the UserSearch configuration were set up such that an end user - should log in using their email address as their username, then - the DryRunAuthenticationUsername should be the actual email address - of a valid user who will be found in the LDAP server by the UserSearch - criteria. Once you have used DryRunAuthenticationUsername to validate - your LDAPIdentityProvider's configuration, you might choose to remove - the DryRunAuthenticationUsername configuration if you are concerned - that the user's LDAP account could change in the future, e.g. if - the account could become disabled in the future. - type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 111a374a..edda79aa 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -757,7 +757,6 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. -| *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0699ddfc..d718ba65 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -100,28 +100,6 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` - - // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. - // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection - // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success - // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. - // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a - // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username - // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for - // that end user during the authentication. This will test all of the configuration options of the - // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the - // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships - // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate - // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". - // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able - // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch - // configuration were set up such that an end user should log in using their email address as their username, then - // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP - // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your - // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration - // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become - // disabled in the future. - DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 5ed9901b..d396129d 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -70,38 +70,6 @@ spec: required: - secretName type: object - dryRunAuthenticationUsername: - description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's - configuration is validated. When DryRunAuthenticationUsername is - blank, the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server using the Host and TLS settings and also will - bind using the Bind settings. The success or failure of the connect - and bind will be reflected in the LDAPIdentityProvider's status - conditions array. When DryRunAuthenticationUsername is not blank, - the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server and performing a full dry run of authenticating - as the end user with the username specified by DryRunAuthenticationUsername. - The dry run will act as if the correct password were specified for - that end user during the authentication. This will test all of the - configuration options of the LDAPIdentityProvider. The success or - failure of the authentication dry run will be reflected in the LDAPIdentityProvider's - status conditions array, along with details of what username, UID, - and group memberships were selected for the specified user. If the - dry run fails, then that user would not be able to authenticate - in a real authentication situation either, so the LDAPIdentityProvider's - Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername - must be a valid username of a real user who should be able to authenticate - given all of the LDAPIdentityProvider's configuration. For example, - if the UserSearch configuration were set up such that an end user - should log in using their email address as their username, then - the DryRunAuthenticationUsername should be the actual email address - of a valid user who will be found in the LDAP server by the UserSearch - criteria. Once you have used DryRunAuthenticationUsername to validate - your LDAPIdentityProvider's configuration, you might choose to remove - the DryRunAuthenticationUsername configuration if you are concerned - that the user's LDAP account could change in the future, e.g. if - the account could become disabled in the future. - type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index f4849bba..cc33968f 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -757,7 +757,6 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. -| *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0699ddfc..d718ba65 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -100,28 +100,6 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` - - // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. - // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection - // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success - // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. - // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a - // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username - // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for - // that end user during the authentication. This will test all of the configuration options of the - // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the - // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships - // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate - // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". - // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able - // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch - // configuration were set up such that an end user should log in using their email address as their username, then - // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP - // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your - // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration - // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become - // disabled in the future. - DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 5ed9901b..d396129d 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -70,38 +70,6 @@ spec: required: - secretName type: object - dryRunAuthenticationUsername: - description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's - configuration is validated. When DryRunAuthenticationUsername is - blank, the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server using the Host and TLS settings and also will - bind using the Bind settings. The success or failure of the connect - and bind will be reflected in the LDAPIdentityProvider's status - conditions array. When DryRunAuthenticationUsername is not blank, - the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server and performing a full dry run of authenticating - as the end user with the username specified by DryRunAuthenticationUsername. - The dry run will act as if the correct password were specified for - that end user during the authentication. This will test all of the - configuration options of the LDAPIdentityProvider. The success or - failure of the authentication dry run will be reflected in the LDAPIdentityProvider's - status conditions array, along with details of what username, UID, - and group memberships were selected for the specified user. If the - dry run fails, then that user would not be able to authenticate - in a real authentication situation either, so the LDAPIdentityProvider's - Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername - must be a valid username of a real user who should be able to authenticate - given all of the LDAPIdentityProvider's configuration. For example, - if the UserSearch configuration were set up such that an end user - should log in using their email address as their username, then - the DryRunAuthenticationUsername should be the actual email address - of a valid user who will be found in the LDAP server by the UserSearch - criteria. Once you have used DryRunAuthenticationUsername to validate - your LDAPIdentityProvider's configuration, you might choose to remove - the DryRunAuthenticationUsername configuration if you are concerned - that the user's LDAP account could change in the future, e.g. if - the account could become disabled in the future. - type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 0a938587..d0effe28 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -757,7 +757,6 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. -| *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0699ddfc..d718ba65 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -100,28 +100,6 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` - - // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. - // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection - // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success - // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. - // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a - // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username - // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for - // that end user during the authentication. This will test all of the configuration options of the - // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the - // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships - // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate - // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". - // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able - // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch - // configuration were set up such that an end user should log in using their email address as their username, then - // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP - // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your - // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration - // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become - // disabled in the future. - DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 5ed9901b..d396129d 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -70,38 +70,6 @@ spec: required: - secretName type: object - dryRunAuthenticationUsername: - description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's - configuration is validated. When DryRunAuthenticationUsername is - blank, the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server using the Host and TLS settings and also will - bind using the Bind settings. The success or failure of the connect - and bind will be reflected in the LDAPIdentityProvider's status - conditions array. When DryRunAuthenticationUsername is not blank, - the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server and performing a full dry run of authenticating - as the end user with the username specified by DryRunAuthenticationUsername. - The dry run will act as if the correct password were specified for - that end user during the authentication. This will test all of the - configuration options of the LDAPIdentityProvider. The success or - failure of the authentication dry run will be reflected in the LDAPIdentityProvider's - status conditions array, along with details of what username, UID, - and group memberships were selected for the specified user. If the - dry run fails, then that user would not be able to authenticate - in a real authentication situation either, so the LDAPIdentityProvider's - Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername - must be a valid username of a real user who should be able to authenticate - given all of the LDAPIdentityProvider's configuration. For example, - if the UserSearch configuration were set up such that an end user - should log in using their email address as their username, then - the DryRunAuthenticationUsername should be the actual email address - of a valid user who will be found in the LDAP server by the UserSearch - criteria. Once you have used DryRunAuthenticationUsername to validate - your LDAPIdentityProvider's configuration, you might choose to remove - the DryRunAuthenticationUsername configuration if you are concerned - that the user's LDAP account could change in the future, e.g. if - the account could become disabled in the future. - type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 805fe2bc..348c55d0 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -757,7 +757,6 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. -| *`dryRunAuthenticationUsername`* __string__ | DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a connection to the LDAP server and performing a full dry run of authenticating as the end user with the username specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for that end user during the authentication. This will test all of the configuration options of the LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships were selected for the specified user. If the dry run fails, then that user would not be able to authenticate in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch configuration were set up such that an end user should log in using their email address as their username, then the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become disabled in the future. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0699ddfc..d718ba65 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -100,28 +100,6 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` - - // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. - // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection - // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success - // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. - // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a - // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username - // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for - // that end user during the authentication. This will test all of the configuration options of the - // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the - // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships - // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate - // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". - // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able - // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch - // configuration were set up such that an end user should log in using their email address as their username, then - // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP - // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your - // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration - // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become - // disabled in the future. - DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 5ed9901b..d396129d 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -70,38 +70,6 @@ spec: required: - secretName type: object - dryRunAuthenticationUsername: - description: DryRunAuthenticationUsername influences how the LDAPIdentityProvider's - configuration is validated. When DryRunAuthenticationUsername is - blank, the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server using the Host and TLS settings and also will - bind using the Bind settings. The success or failure of the connect - and bind will be reflected in the LDAPIdentityProvider's status - conditions array. When DryRunAuthenticationUsername is not blank, - the LDAPIdentityProvider will be validated by opening a connection - to the LDAP server and performing a full dry run of authenticating - as the end user with the username specified by DryRunAuthenticationUsername. - The dry run will act as if the correct password were specified for - that end user during the authentication. This will test all of the - configuration options of the LDAPIdentityProvider. The success or - failure of the authentication dry run will be reflected in the LDAPIdentityProvider's - status conditions array, along with details of what username, UID, - and group memberships were selected for the specified user. If the - dry run fails, then that user would not be able to authenticate - in a real authentication situation either, so the LDAPIdentityProvider's - Status.Phase will be set to "Error". Therefore, the specified DryRunAuthenticationUsername - must be a valid username of a real user who should be able to authenticate - given all of the LDAPIdentityProvider's configuration. For example, - if the UserSearch configuration were set up such that an end user - should log in using their email address as their username, then - the DryRunAuthenticationUsername should be the actual email address - of a valid user who will be found in the LDAP server by the UserSearch - criteria. Once you have used DryRunAuthenticationUsername to validate - your LDAPIdentityProvider's configuration, you might choose to remove - the DryRunAuthenticationUsername configuration if you are concerned - that the user's LDAP account could change in the future, e.g. if - the account could become disabled in the future. - type: string host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0699ddfc..d718ba65 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -100,28 +100,6 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` - - // DryRunAuthenticationUsername influences how the LDAPIdentityProvider's configuration is validated. - // When DryRunAuthenticationUsername is blank, the LDAPIdentityProvider will be validated by opening a connection - // to the LDAP server using the Host and TLS settings and also will bind using the Bind settings. The success - // or failure of the connect and bind will be reflected in the LDAPIdentityProvider's status conditions array. - // When DryRunAuthenticationUsername is not blank, the LDAPIdentityProvider will be validated by opening a - // connection to the LDAP server and performing a full dry run of authenticating as the end user with the username - // specified by DryRunAuthenticationUsername. The dry run will act as if the correct password were specified for - // that end user during the authentication. This will test all of the configuration options of the - // LDAPIdentityProvider. The success or failure of the authentication dry run will be reflected in the - // LDAPIdentityProvider's status conditions array, along with details of what username, UID, and group memberships - // were selected for the specified user. If the dry run fails, then that user would not be able to authenticate - // in a real authentication situation either, so the LDAPIdentityProvider's Status.Phase will be set to "Error". - // Therefore, the specified DryRunAuthenticationUsername must be a valid username of a real user who should be able - // to authenticate given all of the LDAPIdentityProvider's configuration. For example, if the UserSearch - // configuration were set up such that an end user should log in using their email address as their username, then - // the DryRunAuthenticationUsername should be the actual email address of a valid user who will be found in the LDAP - // server by the UserSearch criteria. Once you have used DryRunAuthenticationUsername to validate your - // LDAPIdentityProvider's configuration, you might choose to remove the DryRunAuthenticationUsername configuration - // if you are concerned that the user's LDAP account could change in the future, e.g. if the account could become - // disabled in the future. - DryRunAuthenticationUsername string `json:"dryRunAuthenticationUsername,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index a5009c66..2d7c7d44 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -158,7 +158,6 @@ spec: attributes: uid: "$PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_NAME" username: "$PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME" - dryRunAuthenticationUsername: "$PINNIPED_TEST_LDAP_USER_CN" EOF # Make a Secret for the above LDAPIdentityProvider to describe the bind account. diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go index df7406b0..274cccd4 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go @@ -33,13 +33,12 @@ const ( testLDAPConnectionTimeout = 90 * time.Second // Constants related to conditions. - typeBindSecretValid = "BindSecretValid" - typeTLSConfigurationValid = "TLSConfigurationValid" - typeLDAPConnectionValid = "LDAPConnectionValid" - reasonLDAPConnectionError = "LDAPConnectionError" - reasonAuthenticationDryRunError = "AuthenticationDryRunError" - noTLSConfigurationMessage = "no TLS configuration provided" - loadedTLSConfigurationMessage = "loaded TLS configuration" + typeBindSecretValid = "BindSecretValid" + typeTLSConfigurationValid = "TLSConfigurationValid" + typeLDAPConnectionValid = "LDAPConnectionValid" + reasonLDAPConnectionError = "LDAPConnectionError" + noTLSConfigurationMessage = "no TLS configuration provided" + loadedTLSConfigurationMessage = "loaded TLS configuration" ) var ( @@ -196,10 +195,6 @@ func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upst testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout) defer cancelFunc() - if len(upstream.Spec.DryRunAuthenticationUsername) > 0 { - return c.dryRunAuthentication(testConnectionTimeout, upstream, ldapProvider, currentSecretVersion) - } - return c.testConnection(testConnectionTimeout, upstream, config, ldapProvider, currentSecretVersion) } @@ -230,47 +225,6 @@ func (c *ldapWatcherController) testConnection( } } -func (c *ldapWatcherController) dryRunAuthentication( - ctx context.Context, - upstream *v1alpha1.LDAPIdentityProvider, - ldapProvider *upstreamldap.Provider, - currentSecretVersion string, -) *v1alpha1.Condition { - authResponse, authenticated, err := ldapProvider.DryRunAuthenticateUser(ctx, upstream.Spec.DryRunAuthenticationUsername) - if err != nil { - return &v1alpha1.Condition{ - Type: typeLDAPConnectionValid, - Status: v1alpha1.ConditionFalse, - Reason: reasonAuthenticationDryRunError, - Message: fmt.Sprintf(`failed authentication dry run for end user "%s": %s`, - upstream.Spec.DryRunAuthenticationUsername, err.Error()), - } - } - - if !authenticated { - // Since we aren't doing a real auth with a password that could be wrong, the only reason we should get - // an unauthenticated response without an error is when the username was wrong. - return &v1alpha1.Condition{ - Type: typeLDAPConnectionValid, - Status: v1alpha1.ConditionFalse, - Reason: reasonAuthenticationDryRunError, - Message: fmt.Sprintf(`failed authentication dry run for end user "%s": user not found`, - upstream.Spec.DryRunAuthenticationUsername), - } - } - - return &v1alpha1.Condition{ - Type: typeLDAPConnectionValid, - Status: v1alpha1.ConditionTrue, - Reason: reasonSuccess, - Message: fmt.Sprintf( - `successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`, - upstream.Spec.DryRunAuthenticationUsername, - authResponse.User.GetName(), authResponse.User.GetUID(), - upstream.Spec.Bind.SecretName, currentSecretVersion), - } -} - func hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool { currentGeneration := upstream.Generation for _, c := range upstream.Status.Conditions { diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index 79049037..89f817a9 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -12,8 +12,6 @@ import ( "testing" "time" - "github.com/go-ldap/ldap/v3" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -667,123 +665,6 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }}, }, - { - name: "when DryRunAuthenticationUsername is specified and a successful dry run authentication is performed", - inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { - upstream.Spec.DryRunAuthenticationUsername = "endUserUsername" - })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - setupMocks: func(conn *mockldapconn.MockConn) { - // Should perform a full auth dry run. - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(gomock.Any()).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: "testFoundUserDN", - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUsernameAttrName, []string{"testDownstreamUsername"}), - ldap.NewEntryAttribute(testUIDAttrName, []string{"testDownstreamUID"}), - }, - }, - }, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, - wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, - Status: v1alpha1.LDAPIdentityProviderStatus{ - Phase: "Ready", - Conditions: []v1alpha1.Condition{ - bindSecretValidTrueCondition(1234), - { - Type: "LDAPConnectionValid", - Status: "True", - LastTransitionTime: now, - Reason: "Success", - Message: fmt.Sprintf( - `successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`, - "endUserUsername", "testDownstreamUsername", "testDownstreamUID", testSecretName, "4242"), - ObservedGeneration: 1234, - }, - tlsConfigurationValidLoadedTrueCondition(1234), - }, - }, - }}, - }, - { - name: "when DryRunAuthenticationUsername is specified and the dry run authentication returns an error", - inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { - upstream.Spec.DryRunAuthenticationUsername = "endUserUsername" - })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - setupMocks: func(conn *mockldapconn.MockConn) { - // Failure during a full auth dry run. - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(gomock.Any()).Return(nil, errors.New("some dry run error")).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.ProviderConfig{}, - wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, - Status: v1alpha1.LDAPIdentityProviderStatus{ - Phase: "Error", - Conditions: []v1alpha1.Condition{ - bindSecretValidTrueCondition(1234), - { - Type: "LDAPConnectionValid", - Status: "False", - LastTransitionTime: now, - Reason: "AuthenticationDryRunError", - Message: fmt.Sprintf( - `failed authentication dry run for end user "%s": error searching for user "%s": some dry run error`, - "endUserUsername", "endUserUsername"), - ObservedGeneration: 1234, - }, - tlsConfigurationValidLoadedTrueCondition(1234), - }, - }, - }}, - }, - { - name: "when DryRunAuthenticationUsername is specified and the dry run authentication returns unauthenticated without an error", - inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { - upstream.Spec.DryRunAuthenticationUsername = "endUserUsername" - })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - setupMocks: func(conn *mockldapconn.MockConn) { - // Failure during full auth dry run which will cause it to return unauthenticated instead of error. - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(gomock.Any()).Return(&ldap.SearchResult{ - // No search results means the user did not enter a valid username, which is unauthenticated instead of error. - Entries: []*ldap.Entry{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.ProviderConfig{}, - wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, - Status: v1alpha1.LDAPIdentityProviderStatus{ - Phase: "Error", - Conditions: []v1alpha1.Condition{ - bindSecretValidTrueCondition(1234), - { - Type: "LDAPConnectionValid", - Status: "False", - LastTransitionTime: now, - Reason: "AuthenticationDryRunError", - Message: fmt.Sprintf( - `failed authentication dry run for end user "%s": user not found`, - "endUserUsername"), - ObservedGeneration: 1234, - }, - tlsConfigurationValidLoadedTrueCondition(1234), - }, - }, - }}, - }, } for _, tt := range tests { diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index d9b2fd83..2cba7da0 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -67,7 +67,7 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", }, { - name: "ldap with email as username and with dry run", + name: "ldap with email as username", createIDP: func(t *testing.T) { t.Helper() secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, @@ -92,12 +92,10 @@ func TestSupervisorLogin(t *testing.T) { UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, - DryRunAuthenticationUsername: env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, }, idpv1alpha1.LDAPPhaseReady) expectedMsg := fmt.Sprintf( - `successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`, - env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, - env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue, + `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) @@ -118,7 +116,7 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), }, { - name: "ldap with CN as username and without dry run", // try another variation of configuration options + name: "ldap with CN as username ", // try another variation of configuration options createIDP: func(t *testing.T) { t.Helper() secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, @@ -143,7 +141,6 @@ func TestSupervisorLogin(t *testing.T) { UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, - DryRunAuthenticationUsername: "", // try without dry run }, idpv1alpha1.LDAPPhaseReady) expectedMsg := fmt.Sprintf( `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, From 1c66ffd5ffdf486313c50f5814db8b5a80f35556 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 30 Apr 2021 14:28:03 -0700 Subject: [PATCH 39/59] WIP: add supervisor upstream flags to `pinniped get kubeconfig` - And perform auto-discovery when the flags are not set - Several TODOs remain which will be addressed in the next commit Signed-off-by: Margo Crawford --- cmd/pinniped/cmd/kubeconfig.go | 79 + cmd/pinniped/cmd/kubeconfig_test.go | 2270 ++++++++++++++++++--------- internal/testutil/ioutil.go | 27 +- 3 files changed, 1606 insertions(+), 770 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 50aee669..c1b2e4f7 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -8,8 +8,10 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" + "encoding/json" "fmt" "io" + "io/ioutil" "log" "net/http" "os" @@ -62,6 +64,8 @@ type getKubeconfigOIDCParams struct { debugSessionCache bool caBundle caBundleFlag requestAudience string + upstreamIDPName string + upstreamIDPType string } type getKubeconfigConciergeParams struct { @@ -91,6 +95,15 @@ type getKubeconfigParams struct { credentialCachePathSet bool } +type supervisorDiscoveryResponse struct { + PinnipedIDPs []pinnipedIDPResponse `json:"pinniped_idps"` +} + +type pinnipedIDPResponse struct { + Name string `json:"name"` + Type string `json:"type"` +} + func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { var ( cmd = &cobra.Command{ @@ -128,6 +141,8 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { f.Var(&flags.oidc.caBundle, "oidc-ca-bundle", "Path to TLS certificate authority bundle (PEM format, optional, can be repeated)") f.BoolVar(&flags.oidc.debugSessionCache, "oidc-debug-session-cache", false, "Print debug logs related to the OpenID Connect session cache") f.StringVar(&flags.oidc.requestAudience, "oidc-request-audience", "", "Request a token with an alternate audience using RFC8693 token exchange") + f.StringVar(&flags.oidc.upstreamIDPName, "upstream-identity-provider-name", "", "The name of the upstream identity provider used during login with a Supervisor") + f.StringVar(&flags.oidc.upstreamIDPType, "upstream-identity-provider-type", "", "The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap')") f.StringVar(&flags.kubeconfigPath, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to kubeconfig file") f.StringVar(&flags.kubeconfigContextOverride, "kubeconfig-context", "", "Kubeconfig context name (default: current active context)") f.BoolVar(&flags.skipValidate, "skip-validation", false, "Skip final validation of the kubeconfig (default: false)") @@ -236,6 +251,13 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f cluster.CertificateAuthorityData = flags.concierge.caBundle } + // If there is an issuer, and if both upstream flags are not already set, then try to discover Supervisor upstream IDP. + if len(flags.oidc.issuer) > 0 && (flags.oidc.upstreamIDPType == "" || flags.oidc.upstreamIDPName == "") { + if err := discoverSupervisorUpstreamIDP(ctx, &flags); err != nil { + return err + } + } + // If --credential-cache is set, pass it through. if flags.credentialCachePathSet { execConfig.Args = append(execConfig.Args, "--credential-cache="+flags.credentialCachePath) @@ -289,6 +311,12 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f if flags.oidc.requestAudience != "" { execConfig.Args = append(execConfig.Args, "--request-audience="+flags.oidc.requestAudience) } + if flags.oidc.upstreamIDPName != "" { + execConfig.Args = append(execConfig.Args, "--upstream-identity-provider-name="+flags.oidc.upstreamIDPName) + } + if flags.oidc.upstreamIDPType != "" { + execConfig.Args = append(execConfig.Args, "--upstream-identity-provider-type="+flags.oidc.upstreamIDPType) + } kubeconfig := newExecKubeconfig(cluster, &execConfig, newKubeconfigNames) if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil { return err @@ -688,3 +716,54 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool } return false } + +func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams) error { + issuerDiscoveryURL := flags.oidc.issuer + "/.well-known/openid-configuration" + request, err := http.NewRequestWithContext(ctx, http.MethodGet, issuerDiscoveryURL, nil) + if err != nil { + return fmt.Errorf("while forming request to issuer URL: %w", err) + } + + transport := &http.Transport{TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}} + httpClient := http.Client{Transport: transport} + if flags.oidc.caBundle != nil { + rootCAs := x509.NewCertPool() + ok := rootCAs.AppendCertsFromPEM(flags.oidc.caBundle) + if !ok { + return fmt.Errorf("unable to fetch discovery data from issuer: could not parse CA bundle") + } + transport.TLSClientConfig.RootCAs = rootCAs + } + + response, err := httpClient.Do(request) + if err != nil { + return fmt.Errorf("unable to fetch discovery data from issuer: %w", err) + } + defer func() { + _ = response.Body.Close() + }() + if response.StatusCode == http.StatusNotFound { + // 404 Not Found is not an error because OIDC discovery is an optional part of the OIDC spec. + return nil + } + if response.StatusCode != http.StatusOK { + // Other types of error responses aside from 404 are not expected. + return fmt.Errorf("unable to fetch discovery data from issuer: unexpected http response status: %s", response.Status) + } + + rawBody, err := ioutil.ReadAll(response.Body) + if err != nil { + return fmt.Errorf("unable to fetch discovery data from issuer: could not read response body: %w", err) + } + var body supervisorDiscoveryResponse + err = json.Unmarshal(rawBody, &body) + if err != nil { + return fmt.Errorf("unable to fetch discovery data from issuer: could not parse response JSON: %w", err) + } + + if len(body.PinnipedIDPs) > 0 { + flags.oidc.upstreamIDPName = body.PinnipedIDPs[0].Name + flags.oidc.upstreamIDPType = body.PinnipedIDPs[0].Type + } + return nil +} diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 52384bb4..2efa13d1 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "fmt" "io/ioutil" + "net/http" "path/filepath" "testing" "time" @@ -40,233 +41,315 @@ func TestGetKubeconfig(t *testing.T) { testConciergeCABundlePath := filepath.Join(tmpdir, "testconciergeca.pem") require.NoError(t, ioutil.WriteFile(testConciergeCABundlePath, testConciergeCA.Bundle(), 0600)) + credentialIssuer := func() runtime.Object { + return &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.FetchedKeyStrategyReason, + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, + TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ + Server: "https://concierge-endpoint.example.com", + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + }, + }, + }}, + }, + } + } + + jwtAuthenticator := func(issuerCABundle string, issuerURL string) runtime.Object { + return &conciergev1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, + Spec: conciergev1alpha1.JWTAuthenticatorSpec{ + Issuer: issuerURL, + Audience: "test-audience", + TLS: &conciergev1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(issuerCABundle)), + }, + }, + } + } + tests := []struct { - name string - args []string - env map[string]string - getPathToSelfErr error - getClientsetErr error - conciergeObjects []runtime.Object - conciergeReactions []kubetesting.Reactor - wantLogs []string - wantError bool - wantStdout string - wantStderr string - wantOptionsCount int - wantAPIGroupSuffix string + name string + args func(string, string) []string + env map[string]string + getPathToSelfErr error + getClientsetErr error + conciergeObjects func(string, string) []runtime.Object + conciergeReactions []kubetesting.Reactor + discoveryResponse string + discoveryStatusCode int + wantLogs func(string, string) []string + wantError bool + wantStdout func(string, string) string + wantStderr func(string, string) string + wantOptionsCount int + wantAPIGroupSuffix string }{ { name: "help flag passed", - args: []string{"--help"}, - wantStdout: here.Doc(` + args: func(issuerCABundle string, issuerURL string) []string { return []string{"--help"} }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Doc(` Generate a Pinniped-based kubeconfig for a cluster Usage: kubeconfig [flags] Flags: - --concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev") - --concierge-authenticator-name string Concierge authenticator name (default: autodiscover) - --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover) - --concierge-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge - --concierge-credential-issuer string Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover) - --concierge-endpoint string API base for the Concierge endpoint - --concierge-mode mode Concierge mode of operation (default TokenCredentialRequestAPI) - --concierge-skip-wait Skip waiting for any pending Concierge strategies to become ready (default: false) - --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 - --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 - --oidc-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) - --oidc-client-id string OpenID Connect client ID (default: autodiscover) (default "pinniped-cli") - --oidc-issuer string OpenID Connect issuer URL (default: autodiscover) - --oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only) - --oidc-request-audience string Request a token with an alternate audience using RFC8693 token exchange - --oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience]) - --oidc-session-cache string Path to OpenID Connect session cache file - --oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL) - -o, --output string Output file path (default: stdout) - --skip-validation Skip final validation of the kubeconfig (default: false) - --static-token string Instead of doing an OIDC-based login, specify a static token - --static-token-env string Instead of doing an OIDC-based login, read a static token from the environment - --timeout duration Timeout for autodiscovery and validation (default 10m0s) - `), + --concierge-api-group-suffix string Concierge API group suffix (default "pinniped.dev") + --concierge-authenticator-name string Concierge authenticator name (default: autodiscover) + --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') (default: autodiscover) + --concierge-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) to use when connecting to the Concierge + --concierge-credential-issuer string Concierge CredentialIssuer object to use for autodiscovery (default: autodiscover) + --concierge-endpoint string API base for the Concierge endpoint + --concierge-mode mode Concierge mode of operation (default TokenCredentialRequestAPI) + --concierge-skip-wait Skip waiting for any pending Concierge strategies to become ready (default: false) + --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 + --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 + --oidc-ca-bundle path Path to TLS certificate authority bundle (PEM format, optional, can be repeated) + --oidc-client-id string OpenID Connect client ID (default: autodiscover) (default "pinniped-cli") + --oidc-issuer string OpenID Connect issuer URL (default: autodiscover) + --oidc-listen-port uint16 TCP port for localhost listener (authorization code flow only) + --oidc-request-audience string Request a token with an alternate audience using RFC8693 token exchange + --oidc-scopes strings OpenID Connect scopes to request during login (default [offline_access,openid,pinniped:request-audience]) + --oidc-session-cache string Path to OpenID Connect session cache file + --oidc-skip-browser During OpenID Connect login, skip opening the browser (just print the URL) + -o, --output string Output file path (default: stdout) + --skip-validation Skip final validation of the kubeconfig (default: false) + --static-token string Instead of doing an OIDC-based login, specify a static token + --static-token-env string Instead of doing an OIDC-based login, read a static token from the environment + --timeout duration Timeout for autodiscovery and validation (default 10m0s) + --upstream-identity-provider-name string The name of the upstream identity provider used during login with a Supervisor + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap') + `) + }, }, { name: "fail to get self-path", - args: []string{}, + args: func(issuerCABundle string, issuerURL string) []string { return []string{} }, getPathToSelfErr: fmt.Errorf("some OS error"), wantError: true, - wantStderr: here.Doc(` - Error: could not determine the Pinniped executable path: some OS error - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not determine the Pinniped executable path: some OS error` + "\n" + }, }, { name: "invalid OIDC CA bundle path", - args: []string{ - "--oidc-ca-bundle", "./does/not/exist", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--oidc-ca-bundle", "./does/not/exist", + } }, wantError: true, - wantStderr: here.Doc(` - Error: invalid argument "./does/not/exist" for "--oidc-ca-bundle" flag: could not read CA bundle path: open ./does/not/exist: no such file or directory - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: invalid argument "./does/not/exist" for "--oidc-ca-bundle" flag: could not read CA bundle path: open ./does/not/exist: no such file or directory` + "\n" + }, }, { name: "invalid Concierge CA bundle", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--concierge-ca-bundle", "./does/not/exist", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-ca-bundle", "./does/not/exist", + } }, wantError: true, - wantStderr: here.Doc(` - Error: invalid argument "./does/not/exist" for "--concierge-ca-bundle" flag: could not read CA bundle path: open ./does/not/exist: no such file or directory - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: invalid argument "./does/not/exist" for "--concierge-ca-bundle" flag: could not read CA bundle path: open ./does/not/exist: no such file or directory` + "\n" + }, }, { name: "invalid kubeconfig path", - args: []string{ - "--kubeconfig", "./does/not/exist", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./does/not/exist", + } }, wantError: true, - wantStderr: here.Doc(` - Error: could not load --kubeconfig: stat ./does/not/exist: no such file or directory - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not load --kubeconfig: stat ./does/not/exist: no such file or directory` + "\n" + }, }, { name: "invalid kubeconfig context, missing", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--kubeconfig-context", "invalid", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--kubeconfig-context", "invalid", + } }, wantError: true, - wantStderr: here.Doc(` - Error: could not load --kubeconfig/--kubeconfig-context: no such context "invalid" - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not load --kubeconfig/--kubeconfig-context: no such context "invalid"` + "\n" + }, }, { name: "invalid kubeconfig context, missing cluster", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--kubeconfig-context", "invalid-context-no-such-cluster", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--kubeconfig-context", "invalid-context-no-such-cluster", + } }, wantError: true, - wantStderr: here.Doc(` - Error: could not load --kubeconfig/--kubeconfig-context: no such cluster "invalid-cluster" - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not load --kubeconfig/--kubeconfig-context: no such cluster "invalid-cluster"` + "\n" + }, }, { name: "invalid kubeconfig context, missing user", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--kubeconfig-context", "invalid-context-no-such-user", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--kubeconfig-context", "invalid-context-no-such-user", + } }, wantError: true, - wantStderr: here.Doc(` - Error: could not load --kubeconfig/--kubeconfig-context: no such user "invalid-user" - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not load --kubeconfig/--kubeconfig-context: no such user "invalid-user"` + "\n" + }, }, { name: "clientset creation failure", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, getClientsetErr: fmt.Errorf("some kube error"), wantError: true, - wantStderr: here.Doc(` - Error: could not configure Kubernetes client: some kube error - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not configure Kubernetes client: some kube error` + "\n" + }, }, { name: "no credentialissuers", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, wantError: true, - wantStderr: here.Doc(` - Error: no CredentialIssuers were found - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: no CredentialIssuers were found` + "\n" + }, }, - { name: "credentialissuer not found", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--concierge-credential-issuer", "does-not-exist", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-credential-issuer", "does-not-exist", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + } }, wantError: true, - wantStderr: here.Doc(` - Error: credentialissuers.config.concierge.pinniped.dev "does-not-exist" not found - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: credentialissuers.config.concierge.pinniped.dev "does-not-exist" not found` + "\n" + }, }, { name: "webhook authenticator not found", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--concierge-authenticator-type", "webhook", - "--concierge-authenticator-name", "test-authenticator", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-authenticator-type", "webhook", + "--concierge-authenticator-name", "test-authenticator", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: webhookauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: webhookauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found` + "\n" + }, }, { name: "JWT authenticator not found", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--concierge-authenticator-type", "jwt", - "--concierge-authenticator-name", "test-authenticator", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", "test-authenticator", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: jwtauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: jwtauthenticators.authentication.concierge.pinniped.dev "test-authenticator" not found` + "\n" + }, }, { name: "invalid authenticator type", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--concierge-authenticator-type", "invalid", - "--concierge-authenticator-name", "test-authenticator", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-authenticator-type", "invalid", + "--concierge-authenticator-name", "test-authenticator", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: invalid authenticator type "invalid", supported values are "webhook" and "jwt" - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: invalid authenticator type "invalid", supported values are "webhook" and "jwt"` + "\n" + }, }, { name: "fail to autodetect authenticator, listing jwtauthenticators fails", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + } }, conciergeReactions: []kubetesting.Reactor{ &kubetesting.SimpleReactor{ @@ -278,17 +361,21 @@ func TestGetKubeconfig(t *testing.T) { }, }, wantError: true, - wantStderr: here.Doc(` - Error: failed to list JWTAuthenticator objects for autodiscovery: some list error - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: failed to list JWTAuthenticator objects for autodiscovery: some list error` + "\n" + }, }, { name: "fail to autodetect authenticator, listing webhookauthenticators fails", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + } }, conciergeReactions: []kubetesting.Reactor{ &kubetesting.SimpleReactor{ @@ -299,310 +386,452 @@ func TestGetKubeconfig(t *testing.T) { }, }, }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: failed to list WebhookAuthenticator objects for autodiscovery: some list error - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: failed to list WebhookAuthenticator objects for autodiscovery: some list error` + "\n" + }, }, { name: "fail to autodetect authenticator, none found", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: no authenticators were found - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: no authenticators were found` + "\n" + }, }, { name: "fail to autodetect authenticator, multiple found", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, - &conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-1"}}, - &conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-2"}}, - &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-3"}}, - &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-4"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}}, + &conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-1"}}, + &conciergev1alpha1.JWTAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-2"}}, + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-3"}}, + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator-4"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, - `"level"=0 "msg"="found JWTAuthenticator" "name"="test-authenticator-1"`, - `"level"=0 "msg"="found JWTAuthenticator" "name"="test-authenticator-2"`, - `"level"=0 "msg"="found WebhookAuthenticator" "name"="test-authenticator-3"`, - `"level"=0 "msg"="found WebhookAuthenticator" "name"="test-authenticator-4"`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="found JWTAuthenticator" "name"="test-authenticator-1"`, + `"level"=0 "msg"="found JWTAuthenticator" "name"="test-authenticator-2"`, + `"level"=0 "msg"="found WebhookAuthenticator" "name"="test-authenticator-3"`, + `"level"=0 "msg"="found WebhookAuthenticator" "name"="test-authenticator-4"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: multiple authenticators were found, so the --concierge-authenticator-type/--concierge-authenticator-name flags must be specified` + "\n" + }, }, { name: "autodetect webhook authenticator, bad credential issuer with only failing strategy", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{{ - Type: "SomeType", - Status: configv1alpha1.ErrorStrategyStatus, - Reason: "SomeReason", - Message: "Some message", - }}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: "SomeType", + Status: configv1alpha1.ErrorStrategyStatus, + Reason: "SomeReason", + Message: "Some message", + }}, + }, }, - }, - &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, - `"level"=0 "msg"="found CredentialIssuer strategy" "message"="Some message" "reason"="SomeReason" "status"="Error" "type"="SomeType"`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="found CredentialIssuer strategy" "message"="Some message" "reason"="SomeReason" "status"="Error" "type"="SomeType"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: could not autodiscover --concierge-mode - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not autodiscover --concierge-mode` + "\n" + }, }, { name: "autodetect webhook authenticator, bad credential issuer with invalid impersonation CA", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{ - { - Type: "SomeBrokenType", - Status: configv1alpha1.ErrorStrategyStatus, - Reason: "SomeFailureReason", - Message: "Some error message", - LastUpdateTime: metav1.Now(), - }, - { - Type: "SomeUnknownType", - Status: configv1alpha1.SuccessStrategyStatus, - Reason: "SomeReason", - Message: "Some error message", - LastUpdateTime: metav1.Now(), - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: "SomeUnknownFrontendType", + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + Strategies: []configv1alpha1.CredentialIssuerStrategy{ + { + Type: "SomeBrokenType", + Status: configv1alpha1.ErrorStrategyStatus, + Reason: "SomeFailureReason", + Message: "Some error message", + LastUpdateTime: metav1.Now(), }, - }, - { - Type: "SomeType", - Status: configv1alpha1.SuccessStrategyStatus, - Reason: "SomeReason", - Message: "Some message", - LastUpdateTime: metav1.Now(), - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.ImpersonationProxyFrontendType, - ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ - Endpoint: "https://impersonation-endpoint", - CertificateAuthorityData: "invalid-base-64", + { + Type: "SomeUnknownType", + Status: configv1alpha1.SuccessStrategyStatus, + Reason: "SomeReason", + Message: "Some error message", + LastUpdateTime: metav1.Now(), + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: "SomeUnknownFrontendType", + }, + }, + { + Type: "SomeType", + Status: configv1alpha1.SuccessStrategyStatus, + Reason: "SomeReason", + Message: "Some message", + LastUpdateTime: metav1.Now(), + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.ImpersonationProxyFrontendType, + ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ + Endpoint: "https://impersonation-endpoint", + CertificateAuthorityData: "invalid-base-64", + }, }, }, }, }, }, - }, - &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, - `"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`, - `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-endpoint"`, + 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 impersonation proxy mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-endpoint"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: autodiscovered Concierge CA bundle is invalid: illegal base64 data at input byte 7 - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: autodiscovered Concierge CA bundle is invalid: illegal base64 data at input byte 7` + "\n" + }, }, { name: "autodetect webhook authenticator, missing --oidc-issuer", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{{ - Type: "SomeType", - Status: configv1alpha1.SuccessStrategyStatus, - Reason: "SomeReason", - Message: "Some message", - LastUpdateTime: metav1.Now(), - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, - TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ - Server: "https://concierge-endpoint", - CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", - }, - }, - }}, - }, - }, - &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + } }, - wantLogs: []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"`, + 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"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: could not autodiscover --oidc-issuer and none was provided - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not autodiscover --oidc-issuer and none was provided` + "\n" + }, }, { name: "autodetect JWT authenticator, invalid TLS bundle", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "https://concierge-endpoint", - CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", - }, - Strategies: []configv1alpha1.CredentialIssuerStrategy{{ - Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, - Status: configv1alpha1.SuccessStrategyStatus, - Reason: configv1alpha1.FetchedKeyStrategyReason, - Message: "Successfully fetched key", - LastUpdateTime: metav1.Now(), - // Simulate a previous version of CredentialIssuer that's missing this Frontend field. - Frontend: nil, - }}, - }, - }, - &conciergev1alpha1.JWTAuthenticator{ - ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, - Spec: conciergev1alpha1.JWTAuthenticatorSpec{ - Issuer: "https://test-issuer.example.com", - Audience: "some-test-audience", - TLS: &conciergev1alpha1.TLSSpec{ - CertificateAuthorityData: "invalid-base64", + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ + Server: "https://concierge-endpoint", + CertificateAuthorityData: "ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ==", + }, + Strategies: []configv1alpha1.CredentialIssuerStrategy{{ + Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, + Status: configv1alpha1.SuccessStrategyStatus, + Reason: configv1alpha1.FetchedKeyStrategyReason, + Message: "Successfully fetched key", + LastUpdateTime: metav1.Now(), + // Simulate a previous version of CredentialIssuer that's missing this Frontend field. + Frontend: nil, + }}, }, }, - }, + &conciergev1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, + Spec: conciergev1alpha1.JWTAuthenticatorSpec{ + Issuer: issuerURL, + Audience: "some-test-audience", + TLS: &conciergev1alpha1.TLSSpec{ + CertificateAuthorityData: "invalid-base64", + }, + }, + }, + } }, - wantLogs: []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 JWTAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://test-issuer.example.com"`, - `"level"=0 "msg"="discovered OIDC audience" "audience"="some-test-audience"`, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="some-test-audience"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-authenticator has invalid spec.tls.certificateAuthorityData: illegal base64 data at input byte 7 - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-authenticator has invalid spec.tls.certificateAuthorityData: illegal base64 data at input byte 7` + "\n" + }, }, { name: "invalid static token flags", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--static-token", "test-token", - "--static-token-env", "TEST_TOKEN", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--static-token", "test-token", + "--static-token-env", "TEST_TOKEN", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{{ - Type: configv1alpha1.ImpersonationProxyStrategyType, - Status: configv1alpha1.SuccessStrategyStatus, - Reason: configv1alpha1.ListeningStrategyReason, - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.ImpersonationProxyFrontendType, - ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ - Endpoint: "https://impersonation-proxy-endpoint.example.com", - CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), - }, - }, - }}, - }, - }, - &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, - `"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`, - `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.example.com"`, - `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=1`, - `"level"=0 "msg"="discovered WebhookAuthenticator" "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"`, + } }, wantError: true, - wantStderr: here.Doc(` - Error: only one of --static-token and --static-token-env can be specified - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: only one of --static-token and --static-token-env can be specified` + "\n" + }, }, { name: "invalid API group suffix", - args: []string{ - "--concierge-api-group-suffix", ".starts.with.dot", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--concierge-api-group-suffix", ".starts.with.dot", + } }, wantError: true, - wantStderr: here.Doc(` - Error: invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*') - `), + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: invalid API group suffix: a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')` + "\n" + }, + }, + { + name: "when discovery document 400s", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryStatusCode: http.StatusBadRequest, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return "Error: unable to fetch discovery data from issuer: unexpected http response status: 400 Bad Request\n" + }, + }, + { + name: "when discovery document is not valid JSON", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryStatusCode: http.StatusOK, + discoveryResponse: "this is not valid JSON", + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return "Error: unable to fetch discovery data from issuer: could not parse response JSON: invalid character 'h' in literal true (expecting 'r')\n" + }, + }, + { + name: "when tls information is missing from jwtauthenticator, test fails because discovery fails", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + &conciergev1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, + Spec: conciergev1alpha1.JWTAuthenticatorSpec{ + Issuer: issuerURL, + Audience: "test-audience", + }, + }, + } + }, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return fmt.Sprintf("Error: unable to fetch discovery data from issuer: Get \"%s/.well-known/openid-configuration\": x509: certificate signed by unknown authority\n", issuerURL) + }, + }, + { + name: "when the issuer url is bad", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--oidc-issuer", "https%://bad-issuer-url", // this url cannot be parsed + "--oidc-ca-bundle", f.Name(), + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + &conciergev1alpha1.JWTAuthenticator{ + ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, + Spec: conciergev1alpha1.JWTAuthenticatorSpec{ + Issuer: issuerURL, + Audience: "test-audience", + }, + }, + } + }, + 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 JWTAuthenticator" "name"="test-authenticator"`, + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: while forming request to issuer URL: parse "https%://bad-issuer-url/.well-known/openid-configuration": first path segment in URL cannot contain colon` + "\n" + }, }, { name: "valid static token", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--static-token", "test-token", - "--skip-validation", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--static-token", "test-token", + "--skip-validation", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{{ - Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, - Status: configv1alpha1.SuccessStrategyStatus, - Reason: configv1alpha1.FetchedKeyStrategyReason, - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, - TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ - Server: "https://concierge-endpoint.example.com", - CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), - }, - }, - }}, - }, - }, - &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + } }, - wantLogs: []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"`, + 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: here.Doc(` + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Doc(` apiVersion: v1 clusters: - cluster: @@ -635,44 +864,36 @@ func TestGetKubeconfig(t *testing.T) { command: '.../path/to/pinniped' env: [] provideClusterInfo: true - `), + `) + }, }, { name: "valid static token from env var", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--static-token-env", "TEST_TOKEN", - "--skip-validation", - "--credential-cache", "", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--static-token-env", "TEST_TOKEN", + "--skip-validation", + "--credential-cache", "", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{{ - Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, - Status: configv1alpha1.SuccessStrategyStatus, - Reason: configv1alpha1.FetchedKeyStrategyReason, - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, - TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ - Server: "https://concierge-endpoint.example.com", - CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), - }, - }, - }}, - }, - }, - &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, + } }, - wantLogs: []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"`, + 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: here.Doc(` + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Doc(` apiVersion: v1 clusters: - cluster: @@ -706,401 +927,894 @@ func TestGetKubeconfig(t *testing.T) { command: '.../path/to/pinniped' env: [] provideClusterInfo: true - `), + `) + }, }, { name: "autodetect JWT authenticator", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--skip-validation", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{{ - Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, - Status: configv1alpha1.SuccessStrategyStatus, - Reason: configv1alpha1.FetchedKeyStrategyReason, - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, - TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ - Server: "https://concierge-endpoint.example.com", - CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), - }, - }, - }}, - }, - }, - &conciergev1alpha1.JWTAuthenticator{ - ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, - Spec: conciergev1alpha1.JWTAuthenticatorSpec{ - Issuer: "https://example.com/issuer", - Audience: "test-audience", - TLS: &conciergev1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()), - }, - }, - }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } }, - wantLogs: []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 JWTAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`, - `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, - `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) }, - wantStdout: here.Docf(` - 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 - - oidc - - --enable-concierge - - --concierge-api-group-suffix=pinniped.dev - - --concierge-authenticator-name=test-authenticator - - --concierge-authenticator-type=jwt - - --concierge-endpoint=https://fake-server-url-value - - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== - - --issuer=https://example.com/issuer - - --client-id=pinniped-cli - - --scopes=offline_access,openid,pinniped:request-audience - - --ca-bundle-data=%s - - --request-audience=test-audience - command: '.../path/to/pinniped' - env: [] - provideClusterInfo: true - `, base64.StdEncoding.EncodeToString(testOIDCCA.Bundle())), }, { + name: "autodetect nothing, set a bunch of options", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--concierge-credential-issuer", "test-credential-issuer", - "--concierge-api-group-suffix", "tuna.io", - "--concierge-authenticator-type", "webhook", - "--concierge-authenticator-name", "test-authenticator", - "--concierge-mode", "TokenCredentialRequestAPI", - "--concierge-endpoint", "https://explicit-concierge-endpoint.example.com", - "--concierge-ca-bundle", testConciergeCABundlePath, - "--oidc-issuer", "https://example.com/issuer", - "--oidc-skip-browser", - "--oidc-listen-port", "1234", - "--oidc-ca-bundle", testOIDCCABundlePath, - "--oidc-session-cache", "/path/to/cache/dir/sessions.yaml", - "--oidc-debug-session-cache", - "--oidc-request-audience", "test-audience", - "--skip-validation", - "--generated-name-suffix", "-sso", - "--credential-cache", "/path/to/cache/dir/credentials.yaml", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-credential-issuer", "test-credential-issuer", + "--concierge-api-group-suffix", "tuna.io", + "--concierge-authenticator-type", "webhook", + "--concierge-authenticator-name", "test-authenticator", + "--concierge-mode", "TokenCredentialRequestAPI", + "--concierge-endpoint", "https://explicit-concierge-endpoint.example.com", + "--concierge-ca-bundle", testConciergeCABundlePath, + "--oidc-issuer", issuerURL, + "--oidc-skip-browser", + "--oidc-listen-port", "1234", + "--oidc-ca-bundle", f.Name(), + "--oidc-session-cache", "/path/to/cache/dir/sessions.yaml", + "--oidc-debug-session-cache", + "--oidc-request-audience", "test-audience", + "--skip-validation", + "--generated-name-suffix", "-sso", + "--credential-cache", "/path/to/cache/dir/credentials.yaml", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{{ - Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, - Status: configv1alpha1.SuccessStrategyStatus, - Reason: configv1alpha1.FetchedKeyStrategyReason, - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, - TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ - Server: "https://concierge-endpoint.example.com", - CertificateAuthorityData: "dGVzdC10Y3ItYXBpLWNh", - }, - }, - }}, - }, - }, - &conciergev1alpha1.WebhookAuthenticator{ - ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, - }, + 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 nil }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: %s + server: https://explicit-concierge-endpoint.example.com + name: kind-cluster-sso + contexts: + - context: + cluster: kind-cluster-sso + user: kind-user-sso + name: kind-context-sso + current-context: kind-context-sso + kind: Config + preferences: {} + users: + - name: kind-user-sso + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=tuna.io + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=webhook + - --concierge-endpoint=https://explicit-concierge-endpoint.example.com + - --concierge-ca-bundle-data=%s + - --credential-cache=/path/to/cache/dir/credentials.yaml + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --skip-browser + - --listen-port=1234 + - --ca-bundle-data=%s + - --session-cache=/path/to/cache/dir/sessions.yaml + - --debug-session-cache + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle)), + ) }, - wantLogs: nil, - wantStdout: here.Docf(` - apiVersion: v1 - clusters: - - cluster: - certificate-authority-data: %s - server: https://explicit-concierge-endpoint.example.com - name: kind-cluster-sso - contexts: - - context: - cluster: kind-cluster-sso - user: kind-user-sso - name: kind-context-sso - current-context: kind-context-sso - kind: Config - preferences: {} - users: - - name: kind-user-sso - user: - exec: - apiVersion: client.authentication.k8s.io/v1beta1 - args: - - login - - oidc - - --enable-concierge - - --concierge-api-group-suffix=tuna.io - - --concierge-authenticator-name=test-authenticator - - --concierge-authenticator-type=webhook - - --concierge-endpoint=https://explicit-concierge-endpoint.example.com - - --concierge-ca-bundle-data=%s - - --credential-cache=/path/to/cache/dir/credentials.yaml - - --issuer=https://example.com/issuer - - --client-id=pinniped-cli - - --scopes=offline_access,openid,pinniped:request-audience - - --skip-browser - - --listen-port=1234 - - --ca-bundle-data=%s - - --session-cache=/path/to/cache/dir/sessions.yaml - - --debug-session-cache - - --request-audience=test-audience - command: '.../path/to/pinniped' - env: [] - provideClusterInfo: true - `, - base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), - base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), - base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()), - ), wantAPIGroupSuffix: "tuna.io", }, { name: "configure impersonation proxy with autodiscovered JWT authenticator", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--concierge-mode", "ImpersonationProxy", - "--skip-validation", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--concierge-mode", "ImpersonationProxy", + "--skip-validation", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{ - // This TokenCredentialRequestAPI strategy would normally be chosen, but - // --concierge-mode=ImpersonationProxy should force it to be skipped. - { - Type: "SomeType", - Status: configv1alpha1.SuccessStrategyStatus, - Reason: "SomeReason", - Message: "Some message", - LastUpdateTime: metav1.Now(), - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, - TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ - Server: "https://token-credential-request-api-endpoint.test", - CertificateAuthorityData: "dGVzdC10Y3ItYXBpLWNh", + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + Strategies: []configv1alpha1.CredentialIssuerStrategy{ + // This TokenCredentialRequestAPI strategy would normally be chosen, but + // --concierge-mode=ImpersonationProxy should force it to be skipped. + { + Type: "SomeType", + Status: configv1alpha1.SuccessStrategyStatus, + Reason: "SomeReason", + Message: "Some message", + LastUpdateTime: metav1.Now(), + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.TokenCredentialRequestAPIFrontendType, + TokenCredentialRequestAPIInfo: &configv1alpha1.TokenCredentialRequestAPIInfo{ + Server: "https://token-credential-request-api-endpoint.test", + CertificateAuthorityData: "dGVzdC10Y3ItYXBpLWNh", + }, }, }, - }, - // The endpoint and CA from this impersonation proxy strategy should be autodiscovered. - { - Type: "SomeOtherType", - Status: configv1alpha1.SuccessStrategyStatus, - Reason: "SomeOtherReason", - Message: "Some other message", - LastUpdateTime: metav1.Now(), - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.ImpersonationProxyFrontendType, - ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ - Endpoint: "https://impersonation-proxy-endpoint.test", - CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + // The endpoint and CA from this impersonation proxy strategy should be autodiscovered. + { + Type: "SomeOtherType", + Status: configv1alpha1.SuccessStrategyStatus, + Reason: "SomeOtherReason", + Message: "Some other message", + LastUpdateTime: metav1.Now(), + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.ImpersonationProxyFrontendType, + ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ + Endpoint: "https://impersonation-proxy-endpoint.test", + CertificateAuthorityData: base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + }, }, }, }, }, }, - }, - &conciergev1alpha1.JWTAuthenticator{ - ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, - Spec: conciergev1alpha1.JWTAuthenticatorSpec{ - Issuer: "https://example.com/issuer", - Audience: "test-audience", - TLS: &conciergev1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()), - }, - }, - }, + jwtAuthenticator(issuerCABundle, issuerURL), + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, - `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, - `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=1`, - `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`, - `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, - `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + wantLogs: func(issuerCABundle string, issuerURL string) []string { + return []string{ + `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=1`, + `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: %s + server: https://impersonation-proxy-endpoint.test + name: kind-cluster-pinniped + contexts: + - context: + cluster: kind-cluster-pinniped + user: kind-user-pinniped + name: kind-context-pinniped + current-context: kind-context-pinniped + kind: Config + preferences: {} + users: + - name: kind-user-pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://impersonation-proxy-endpoint.test + - --concierge-ca-bundle-data=%s + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle)), + ) }, - wantStdout: here.Docf(` - apiVersion: v1 - clusters: - - cluster: - certificate-authority-data: %s - server: https://impersonation-proxy-endpoint.test - name: kind-cluster-pinniped - contexts: - - context: - cluster: kind-cluster-pinniped - user: kind-user-pinniped - name: kind-context-pinniped - current-context: kind-context-pinniped - kind: Config - preferences: {} - users: - - name: kind-user-pinniped - user: - exec: - apiVersion: client.authentication.k8s.io/v1beta1 - args: - - login - - oidc - - --enable-concierge - - --concierge-api-group-suffix=pinniped.dev - - --concierge-authenticator-name=test-authenticator - - --concierge-authenticator-type=jwt - - --concierge-endpoint=https://impersonation-proxy-endpoint.test - - --concierge-ca-bundle-data=%s - - --issuer=https://example.com/issuer - - --client-id=pinniped-cli - - --scopes=offline_access,openid,pinniped:request-audience - - --ca-bundle-data=%s - - --request-audience=test-audience - command: '.../path/to/pinniped' - env: [] - provideClusterInfo: true - `, - base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), - base64.StdEncoding.EncodeToString(testConciergeCA.Bundle()), - base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()), - ), }, { name: "autodetect impersonation proxy with autodiscovered JWT authenticator", - args: []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--skip-validation", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } }, - conciergeObjects: []runtime.Object{ - &configv1alpha1.CredentialIssuer{ - ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{ - { - Type: "SomeType", - Status: configv1alpha1.SuccessStrategyStatus, - Reason: "SomeReason", - Message: "Some message", - LastUpdateTime: metav1.Now(), - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.ImpersonationProxyFrontendType, - ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ - Endpoint: "https://impersonation-proxy-endpoint.test", - CertificateAuthorityData: "dGVzdC1jb25jaWVyZ2UtY2E=", + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + &configv1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: "test-credential-issuer"}, + Status: configv1alpha1.CredentialIssuerStatus{ + Strategies: []configv1alpha1.CredentialIssuerStrategy{ + { + Type: "SomeType", + Status: configv1alpha1.SuccessStrategyStatus, + Reason: "SomeReason", + Message: "Some message", + LastUpdateTime: metav1.Now(), + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.ImpersonationProxyFrontendType, + ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ + Endpoint: "https://impersonation-proxy-endpoint.test", + CertificateAuthorityData: "dGVzdC1jb25jaWVyZ2UtY2E=", + }, }, }, - }, - { - Type: "SomeOtherType", - Status: configv1alpha1.SuccessStrategyStatus, - Reason: "SomeOtherReason", - Message: "Some other message", - LastUpdateTime: metav1.Now(), - Frontend: &configv1alpha1.CredentialIssuerFrontend{ - Type: configv1alpha1.ImpersonationProxyFrontendType, - ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ - Endpoint: "https://some-other-impersonation-endpoint", - CertificateAuthorityData: "dGVzdC1jb25jaWVyZ2UtY2E=", + { + Type: "SomeOtherType", + Status: configv1alpha1.SuccessStrategyStatus, + Reason: "SomeOtherReason", + Message: "Some other message", + LastUpdateTime: metav1.Now(), + Frontend: &configv1alpha1.CredentialIssuerFrontend{ + Type: configv1alpha1.ImpersonationProxyFrontendType, + ImpersonationProxyInfo: &configv1alpha1.ImpersonationProxyInfo{ + Endpoint: "https://some-other-impersonation-endpoint", + CertificateAuthorityData: "dGVzdC1jb25jaWVyZ2UtY2E=", + }, }, }, }, }, }, - }, - &conciergev1alpha1.JWTAuthenticator{ - ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}, - Spec: conciergev1alpha1.JWTAuthenticatorSpec{ - Issuer: "https://example.com/issuer", - Audience: "test-audience", - TLS: &conciergev1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString(testOIDCCA.Bundle()), - }, - }, - }, + jwtAuthenticator(issuerCABundle, issuerURL), + } }, - wantLogs: []string{ - `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, - `"level"=0 "msg"="discovered Concierge operating in impersonation proxy mode"`, - `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, - `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`, - `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, - `"level"=0 "msg"="discovered OIDC issuer" "issuer"="https://example.com/issuer"`, - `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, - `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + 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 impersonation proxy mode"`, + `"level"=0 "msg"="discovered Concierge endpoint" "endpoint"="https://impersonation-proxy-endpoint.test"`, + `"level"=0 "msg"="discovered Concierge certificate authority bundle" "roots"=0`, + `"level"=0 "msg"="discovered JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: dGVzdC1jb25jaWVyZ2UtY2E= + server: https://impersonation-proxy-endpoint.test + name: kind-cluster-pinniped + contexts: + - context: + cluster: kind-cluster-pinniped + user: kind-user-pinniped + name: kind-context-pinniped + current-context: kind-context-pinniped + kind: Config + preferences: {} + users: + - name: kind-user-pinniped + user: + exec: + apiVersion: client.authentication.k8s.io/v1beta1 + args: + - login + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://impersonation-proxy-endpoint.test + - --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E= + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "Find LDAP idp in discovery document, output ldap related flags", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [{"name": "some-ldap-idp", "type": "ldap"}] + }`), + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + - --upstream-identity-provider-name=some-ldap-idp + - --upstream-identity-provider-type=ldap + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "Find OIDC idp in discovery document, output oidc related flags", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [{"name": "some-oidc-idp", "type": "oidc"}] + }`), + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + - --upstream-identity-provider-name=some-oidc-idp + - --upstream-identity-provider-type=oidc + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "empty idp list in discovery document", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [] + }`), + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "when discovery document 404s, dont set idp related flags", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryStatusCode: http.StatusNotFound, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "when upstream idp related flags are sent, pass them through", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--upstream-identity-provider-name=some-oidc-idp", + "--upstream-identity-provider-type=oidc", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryStatusCode: http.StatusNotFound, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + - --upstream-identity-provider-name=some-oidc-idp + - --upstream-identity-provider-type=oidc + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "when upstream idp related flags are sent, pass them through even when discovery shows a different idp", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--upstream-identity-provider-name=some-oidc-idp", + "--upstream-identity-provider-type=oidc", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [{"name": "some-other-ldap-idp", "type": "ldap"}] + }`), + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + - --upstream-identity-provider-name=some-oidc-idp + - --upstream-identity-provider-type=oidc + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "supervisor upstream IDP discovery still works when --no-concierge is used", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--no-concierge", + "--oidc-issuer", issuerURL, + "--oidc-ca-bundle", f.Name(), + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [{"name": "some-ldap-idp", "type": "ldap"}] + }`), + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --upstream-identity-provider-name=some-ldap-idp + - --upstream-identity-provider-type=ldap + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) }, - wantStdout: here.Docf(` - apiVersion: v1 - clusters: - - cluster: - certificate-authority-data: dGVzdC1jb25jaWVyZ2UtY2E= - server: https://impersonation-proxy-endpoint.test - name: kind-cluster-pinniped - contexts: - - context: - cluster: kind-cluster-pinniped - user: kind-user-pinniped - name: kind-context-pinniped - current-context: kind-context-pinniped - kind: Config - preferences: {} - users: - - name: kind-user-pinniped - user: - exec: - apiVersion: client.authentication.k8s.io/v1beta1 - args: - - login - - oidc - - --enable-concierge - - --concierge-api-group-suffix=pinniped.dev - - --concierge-authenticator-name=test-authenticator - - --concierge-authenticator-type=jwt - - --concierge-endpoint=https://impersonation-proxy-endpoint.test - - --concierge-ca-bundle-data=dGVzdC1jb25jaWVyZ2UtY2E= - - --issuer=https://example.com/issuer - - --client-id=pinniped-cli - - --scopes=offline_access,openid,pinniped:request-audience - - --ca-bundle-data=%s - - --request-audience=test-audience - command: '.../path/to/pinniped' - env: [] - provideClusterInfo: true - `, base64.StdEncoding.EncodeToString(testOIDCCA.Bundle())), }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { + // TODO multiple idps should error + // TODO partial discovery: specify issuer, don't specify idp type or name + // TODO if only idp type or only idp name is specified, not both, still do discovery and do some fancy checking or something + // TODO logging the values we discover? + + issuerCABundle, issuerEndpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/.well-known/openid-configuration" { + jsonResponseBody := tt.discoveryResponse + if tt.discoveryResponse == "" { + jsonResponseBody = "{}" + } + if tt.discoveryStatusCode == 0 { + tt.discoveryStatusCode = http.StatusOK + } + w.WriteHeader(tt.discoveryStatusCode) + _, err = w.Write([]byte(jsonResponseBody)) + require.NoError(t, err) + } else { + t.Fatalf("tried to call issuer at a path that wasn't the discovery endpoint.") + } + }) + testLog := testlogger.New(t) cmd := kubeconfigCommand(kubeconfigDeps{ getPathToSelf: func() (string, error) { @@ -1118,7 +1832,10 @@ func TestGetKubeconfig(t *testing.T) { if tt.getClientsetErr != nil { return nil, tt.getClientsetErr } - fake := fakeconciergeclientset.NewSimpleClientset(tt.conciergeObjects...) + fake := fakeconciergeclientset.NewSimpleClientset() + if tt.conciergeObjects != nil { + fake = fakeconciergeclientset.NewSimpleClientset(tt.conciergeObjects(issuerCABundle, issuerEndpoint)...) + } if len(tt.conciergeReactions) > 0 { fake.ReactionChain = append(tt.conciergeReactions, fake.ReactionChain...) } @@ -1131,16 +1848,33 @@ func TestGetKubeconfig(t *testing.T) { var stdout, stderr bytes.Buffer cmd.SetOut(&stdout) cmd.SetErr(&stderr) - cmd.SetArgs(tt.args) + + cmd.SetArgs(tt.args(issuerCABundle, issuerEndpoint)) + err := cmd.Execute() if tt.wantError { require.Error(t, err) } else { require.NoError(t, err) } - testLog.Expect(tt.wantLogs) - require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout") - require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr") + + var expectedLogs []string + if tt.wantLogs != nil { + expectedLogs = tt.wantLogs(issuerCABundle, issuerEndpoint) + } + testLog.Expect(expectedLogs) + + expectedStdout := "" + if tt.wantStdout != nil { + expectedStdout = tt.wantStdout(issuerCABundle, issuerEndpoint) + } + require.Equal(t, expectedStdout, stdout.String(), "unexpected stdout") + + expectedStderr := "" + if tt.wantStderr != nil { + expectedStderr = tt.wantStderr(issuerCABundle, issuerEndpoint) + } + require.Equal(t, expectedStderr, stderr.String(), "unexpected stderr") }) } } diff --git a/internal/testutil/ioutil.go b/internal/testutil/ioutil.go index ebe9891a..9b1f086f 100644 --- a/internal/testutil/ioutil.go +++ b/internal/testutil/ioutil.go @@ -1,9 +1,16 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package testutil -import "io" +import ( + "io" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/require" +) // ErrorWriter implements io.Writer by returning a fixed error. type ErrorWriter struct { @@ -13,3 +20,19 @@ type ErrorWriter struct { var _ io.Writer = &ErrorWriter{} func (e *ErrorWriter) Write([]byte) (int, error) { return 0, e.ReturnError } + +func WriteStringToTempFile(t *testing.T, filename string, fileBody string) *os.File { + t.Helper() + f, err := ioutil.TempFile("", filename) + require.NoError(t, err) + deferMe := func() { + err := os.Remove(f.Name()) + require.NoError(t, err) + } + t.Cleanup(deferMe) + _, err = f.WriteString(fileBody) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + return f +} From a8754b56580f4cf02eb13066bb5ebc13a65dd68f Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Fri, 30 Apr 2021 15:00:54 -0700 Subject: [PATCH 40/59] Refactor: extract helper func from `runGetKubeconfig()` - Reduces the cyclomatic complexity of the function Signed-off-by: Ryan Richard --- cmd/pinniped/cmd/kubeconfig.go | 79 ++++++++++++++++------------- cmd/pinniped/cmd/kubeconfig_test.go | 40 +++++++++++---- 2 files changed, 75 insertions(+), 44 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index c1b2e4f7..3f4afaf5 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -180,19 +180,6 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f return fmt.Errorf("invalid API group suffix: %w", err) } - execConfig := clientcmdapi.ExecConfig{ - APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(), - Args: []string{}, - Env: []clientcmdapi.ExecEnvVar{}, - } - - var err error - execConfig.Command, err = deps.getPathToSelf() - if err != nil { - return fmt.Errorf("could not determine the Pinniped executable path: %w", err) - } - execConfig.ProvideClusterInfo = true - clientConfig := newClientConfig(flags.kubeconfigPath, flags.kubeconfigContextOverride) currentKubeConfig, err := clientConfig.RawConfig() if err != nil { @@ -236,15 +223,6 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f if err := discoverAuthenticatorParams(authenticator, &flags, deps.log); err != nil { return err } - // Append the flags to configure the Concierge credential exchange at runtime. - execConfig.Args = append(execConfig.Args, - "--enable-concierge", - "--concierge-api-group-suffix="+flags.concierge.apiGroupSuffix, - "--concierge-authenticator-name="+flags.concierge.authenticatorName, - "--concierge-authenticator-type="+flags.concierge.authenticatorType, - "--concierge-endpoint="+flags.concierge.endpoint, - "--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(flags.concierge.caBundle), - ) // Point kubectl at the concierge endpoint. cluster.Server = flags.concierge.endpoint @@ -258,6 +236,45 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f } } + execConfig, err := newExecConfig(deps, flags) + if err != nil { + return err + } + + kubeconfig := newExecKubeconfig(cluster, execConfig, newKubeconfigNames) + if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil { + return err + } + + return writeConfigAsYAML(out, kubeconfig) +} + +func newExecConfig(deps kubeconfigDeps, flags getKubeconfigParams) (*clientcmdapi.ExecConfig, error) { + execConfig := &clientcmdapi.ExecConfig{ + APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(), + Args: []string{}, + Env: []clientcmdapi.ExecEnvVar{}, + ProvideClusterInfo: true, + } + + var err error + execConfig.Command, err = deps.getPathToSelf() + if err != nil { + return nil, fmt.Errorf("could not determine the Pinniped executable path: %w", err) + } + + if !flags.concierge.disabled { + // Append the flags to configure the Concierge credential exchange at runtime. + execConfig.Args = append(execConfig.Args, + "--enable-concierge", + "--concierge-api-group-suffix="+flags.concierge.apiGroupSuffix, + "--concierge-authenticator-name="+flags.concierge.authenticatorName, + "--concierge-authenticator-type="+flags.concierge.authenticatorType, + "--concierge-endpoint="+flags.concierge.endpoint, + "--concierge-ca-bundle-data="+base64.StdEncoding.EncodeToString(flags.concierge.caBundle), + ) + } + // If --credential-cache is set, pass it through. if flags.credentialCachePathSet { execConfig.Args = append(execConfig.Args, "--credential-cache="+flags.credentialCachePath) @@ -266,7 +283,7 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f // If one of the --static-* flags was passed, output a config that runs `pinniped login static`. if flags.staticToken != "" || flags.staticTokenEnvName != "" { if flags.staticToken != "" && flags.staticTokenEnvName != "" { - return fmt.Errorf("only one of --static-token and --static-token-env can be specified") + return nil, fmt.Errorf("only one of --static-token and --static-token-env can be specified") } execConfig.Args = append([]string{"login", "static"}, execConfig.Args...) if flags.staticToken != "" { @@ -275,18 +292,13 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f if flags.staticTokenEnvName != "" { execConfig.Args = append(execConfig.Args, "--token-env="+flags.staticTokenEnvName) } - - kubeconfig := newExecKubeconfig(cluster, &execConfig, newKubeconfigNames) - if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil { - return err - } - return writeConfigAsYAML(out, kubeconfig) + return execConfig, nil } // Otherwise continue to parse the OIDC-related flags and output a config that runs `pinniped login oidc`. execConfig.Args = append([]string{"login", "oidc"}, execConfig.Args...) if flags.oidc.issuer == "" { - return fmt.Errorf("could not autodiscover --oidc-issuer and none was provided") + return nil, fmt.Errorf("could not autodiscover --oidc-issuer and none was provided") } execConfig.Args = append(execConfig.Args, "--issuer="+flags.oidc.issuer, @@ -317,11 +329,8 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f if flags.oidc.upstreamIDPType != "" { execConfig.Args = append(execConfig.Args, "--upstream-identity-provider-type="+flags.oidc.upstreamIDPType) } - kubeconfig := newExecKubeconfig(cluster, &execConfig, newKubeconfigNames) - if err := validateKubeconfig(ctx, flags, kubeconfig, deps.log); err != nil { - return err - } - return writeConfigAsYAML(out, kubeconfig) + + return execConfig, nil } type kubeconfigNames struct{ ContextName, UserName, ClusterName string } diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 2efa13d1..51d9e169 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -134,15 +134,6 @@ func TestGetKubeconfig(t *testing.T) { `) }, }, - { - name: "fail to get self-path", - args: func(issuerCABundle string, issuerURL string) []string { return []string{} }, - getPathToSelfErr: fmt.Errorf("some OS error"), - wantError: true, - wantStderr: func(issuerCABundle string, issuerURL string) string { - return `Error: could not determine the Pinniped executable path: some OS error` + "\n" - }, - }, { name: "invalid OIDC CA bundle path", args: func(issuerCABundle string, issuerURL string) []string { @@ -626,6 +617,37 @@ func TestGetKubeconfig(t *testing.T) { return `Error: tried to autodiscover --oidc-ca-bundle, but JWTAuthenticator test-authenticator has invalid spec.tls.certificateAuthorityData: illegal base64 data at input byte 7` + "\n" }, }, + { + name: "fail to get self-path", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + } + }, + getPathToSelfErr: fmt.Errorf("some OS error"), + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: could not determine the Pinniped executable path: some OS error` + "\n" + }, + }, { name: "invalid static token flags", args: func(issuerCABundle string, issuerURL string) []string { From 778c194cc46a682cccd9c554926bf63c620d85be Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Fri, 30 Apr 2021 17:14:28 -0700 Subject: [PATCH 41/59] Autodetection with multiple idps in discovery document Signed-off-by: Ryan Richard --- cmd/pinniped/cmd/kubeconfig.go | 65 ++++++- cmd/pinniped/cmd/kubeconfig_test.go | 266 +++++++++++++++++++++++++++- test/integration/e2e_test.go | 3 +- 3 files changed, 326 insertions(+), 8 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 3f4afaf5..d59b61ba 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -733,7 +733,10 @@ func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigPara return fmt.Errorf("while forming request to issuer URL: %w", err) } - transport := &http.Transport{TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}} + transport := &http.Transport{ + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + Proxy: http.ProxyFromEnvironment, + } httpClient := http.Client{Transport: transport} if flags.oidc.caBundle != nil { rootCAs := x509.NewCertPool() @@ -770,9 +773,67 @@ func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigPara return fmt.Errorf("unable to fetch discovery data from issuer: could not parse response JSON: %w", err) } - if len(body.PinnipedIDPs) > 0 { + if len(body.PinnipedIDPs) == 1 { flags.oidc.upstreamIDPName = body.PinnipedIDPs[0].Name flags.oidc.upstreamIDPType = body.PinnipedIDPs[0].Type + } else if len(body.PinnipedIDPs) > 1 { + idpName, idpType, err := selectUpstreamIDP(body.PinnipedIDPs, flags.oidc.upstreamIDPName, flags.oidc.upstreamIDPType) + if err != nil { + return err + } + flags.oidc.upstreamIDPName = idpName + flags.oidc.upstreamIDPType = idpType } return nil } + +func selectUpstreamIDP(pinnipedIDPs []pinnipedIDPResponse, idpName, idpType string) (string, string, error) { + pinnipedIDPsString, _ := json.Marshal(pinnipedIDPs) + switch { + case idpType != "": + discoveredName := "" + for _, idp := range pinnipedIDPs { + if idp.Type == idpType { + if discoveredName != "" { + return "", "", fmt.Errorf( + "multiple Supervisor upstream identity providers of type \"%s\" were found,"+ + " so the --upstream-identity-provider-name flag must be specified. "+ + "Found these upstreams: %s", + idpType, pinnipedIDPsString) + } + discoveredName = idp.Name + } + } + if discoveredName == "" { + return "", "", fmt.Errorf( + "no Supervisor upstream identity providers of type \"%s\" were found."+ + " Found these upstreams: %s", idpType, pinnipedIDPsString) + } + return discoveredName, idpType, nil + case idpName != "": + discoveredType := "" + for _, idp := range pinnipedIDPs { + if idp.Name == idpName { + if discoveredType != "" { + return "", "", fmt.Errorf( + "multiple Supervisor upstream identity providers with name \"%s\" were found,"+ + " so the --upstream-identity-provider-type flag must be specified. Found these upstreams: %s", + idpName, pinnipedIDPsString) + } + discoveredType = idp.Type + } + } + if discoveredType == "" { + return "", "", fmt.Errorf( + "no Supervisor upstream identity providers with name \"%s\" were found."+ + " Found these upstreams: %s", idpName, pinnipedIDPsString) + } + return idpName, discoveredType, nil + default: + return "", "", fmt.Errorf( + "multiple Supervisor upstream identity providers were found,"+ + " so the --upstream-identity-provider-name/--upstream-identity-provider-type flags must be specified."+ + " Found these upstreams: %s", + pinnipedIDPsString) + } +} diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 51d9e169..e244e14b 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -721,6 +721,46 @@ func TestGetKubeconfig(t *testing.T) { return "Error: unable to fetch discovery data from issuer: unexpected http response status: 400 Bad Request\n" }, }, + { + name: "when discovery document contains multiple pinniped_idps and no name or type flags are given", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + discoveryStatusCode: http.StatusOK, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [ + {"name": "some-ldap-idp", "type": "ldap"}, + {"name": "some-oidc-idp", "type": "oidc"} + ] + }`), + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: multiple Supervisor upstream identity providers were found, ` + + `so the --upstream-identity-provider-name/--upstream-identity-provider-type flags must be specified. ` + + `Found these upstreams: [{"name":"some-ldap-idp","type":"ldap"},{"name":"some-oidc-idp","type":"oidc"}]` + "\n" + }, + }, { name: "when discovery document is not valid JSON", args: func(issuerCABundle string, issuerURL string) []string { @@ -828,6 +868,111 @@ func TestGetKubeconfig(t *testing.T) { return `Error: while forming request to issuer URL: parse "https%://bad-issuer-url/.well-known/openid-configuration": first path segment in URL cannot contain colon` + "\n" }, }, + { + name: "supervisor upstream IDP discovery fails to resolve ambiguity when type is specified but name is not", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--no-concierge", + "--oidc-issuer", issuerURL, + "--oidc-ca-bundle", f.Name(), + "--upstream-identity-provider-type", "ldap", + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [ + {"name": "some-ldap-idp", "type": "ldap"}, + {"name": "some-other-ldap-idp", "type": "ldap"}, + {"name": "some-oidc-idp", "type": "oidc"}, + {"name": "some-other-oidc-idp", "type": "oidc"} + ] + }`), + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: multiple Supervisor upstream identity providers of type "ldap" were found,` + + ` so the --upstream-identity-provider-name flag must be specified.` + + ` Found these upstreams: [{"name":"some-ldap-idp","type":"ldap"},{"name":"some-other-ldap-idp","type":"ldap"},{"name":"some-oidc-idp","type":"oidc"},{"name":"some-other-oidc-idp","type":"oidc"}]` + "\n" + }, + }, + { + name: "supervisor upstream IDP discovery fails to resolve ambiguity when name is specified but type is not", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--no-concierge", + "--oidc-issuer", issuerURL, + "--oidc-ca-bundle", f.Name(), + "--upstream-identity-provider-name", "my-idp", + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [ + {"name": "my-idp", "type": "ldap"}, + {"name": "my-idp", "type": "oidc"}, + {"name": "some-other-oidc-idp", "type": "oidc"} + ] + }`), + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: multiple Supervisor upstream identity providers with name "my-idp" were found,` + + ` so the --upstream-identity-provider-type flag must be specified.` + + ` Found these upstreams: [{"name":"my-idp","type":"ldap"},{"name":"my-idp","type":"oidc"},{"name":"some-other-oidc-idp","type":"oidc"}]` + "\n" + }, + }, + { + name: "supervisor upstream IDP discovery fails to find any matching idps when type is specified but name is not", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--no-concierge", + "--oidc-issuer", issuerURL, + "--oidc-ca-bundle", f.Name(), + "--upstream-identity-provider-type", "ldap", + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [ + {"name": "some-oidc-idp", "type": "oidc"}, + {"name": "some-other-oidc-idp", "type": "oidc"} + ] + }`), + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: no Supervisor upstream identity providers of type "ldap" were found.` + + ` Found these upstreams: [{"name":"some-oidc-idp","type":"oidc"},{"name":"some-other-oidc-idp","type":"oidc"}]` + "\n" + }, + }, + { + name: "supervisor upstream IDP discovery fails to find any matching idps when name is specified but type is not", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--no-concierge", + "--oidc-issuer", issuerURL, + "--oidc-ca-bundle", f.Name(), + "--upstream-identity-provider-name", "my-nonexistent-idp", + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [ + {"name": "some-oidc-idp", "type": "oidc"}, + {"name": "some-other-oidc-idp", "type": "oidc"} + ] + }`), + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: no Supervisor upstream identity providers with name "my-nonexistent-idp" were found.` + + ` Found these upstreams: [{"name":"some-oidc-idp","type":"oidc"},{"name":"some-other-oidc-idp","type":"oidc"}]` + "\n" + }, + }, { name: "valid static token", args: func(issuerCABundle string, issuerURL string) []string { @@ -1811,15 +1956,126 @@ func TestGetKubeconfig(t *testing.T) { base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) }, }, + { + name: "supervisor upstream IDP discovery resolves ambiguity when type is specified but name is not", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--no-concierge", + "--oidc-issuer", issuerURL, + "--oidc-ca-bundle", f.Name(), + "--upstream-identity-provider-type", "ldap", + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [ + {"name": "some-ldap-idp", "type": "ldap"}, + {"name": "some-oidc-idp", "type": "oidc"}, + {"name": "some-other-oidc-idp", "type": "oidc"} + ] + }`), + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --upstream-identity-provider-name=some-ldap-idp + - --upstream-identity-provider-type=ldap + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "supervisor upstream IDP discovery resolves ambiguity when name is specified but type is not", + args: func(issuerCABundle string, issuerURL string) []string { + f := testutil.WriteStringToTempFile(t, "testca-*.pem", issuerCABundle) + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + "--no-concierge", + "--oidc-issuer", issuerURL, + "--oidc-ca-bundle", f.Name(), + "--upstream-identity-provider-name", "some-ldap-idp", + } + }, + discoveryResponse: here.Docf(`{ + "pinniped_idps": [ + {"name": "some-ldap-idp", "type": "ldap"}, + {"name": "some-oidc-idp", "type": "oidc"}, + {"name": "some-other-oidc-idp", "type": "oidc"} + ] + }`), + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --upstream-identity-provider-name=some-ldap-idp + - --upstream-identity-provider-type=ldap + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - // TODO multiple idps should error - // TODO partial discovery: specify issuer, don't specify idp type or name - // TODO if only idp type or only idp name is specified, not both, still do discovery and do some fancy checking or something - // TODO logging the values we discover? - issuerCABundle, issuerEndpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/.well-known/openid-configuration" { jsonResponseBody := tt.discoveryResponse diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 7c357711..416b6081 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -134,7 +134,8 @@ func TestE2EFullIntegration(t *testing.T) { sessionCachePath := tempDir + "/sessions.yaml" // Run "pinniped get kubeconfig" to get a kubeconfig YAML. - kubeconfigYAML, stderr := runPinnipedCLI(t, nil, pinnipedExe, "get", "kubeconfig", + envVarsWithProxy := append(os.Environ(), env.ProxyEnv()...) + kubeconfigYAML, stderr := runPinnipedCLI(t, envVarsWithProxy, pinnipedExe, "get", "kubeconfig", "--concierge-api-group-suffix", env.APIGroupSuffix, "--concierge-authenticator-type", "jwt", "--concierge-authenticator-name", authenticator.Name, From c0fcd275948f021c319c752552bbf18dbd8df1f5 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 10 May 2021 12:51:56 -0700 Subject: [PATCH 42/59] Fix typo in test/integration/e2e_test.go Co-authored-by: Mo Khan --- test/integration/e2e_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 7c357711..b2fcbe36 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -327,8 +327,8 @@ status: `, string(kubectlOutput3)) - expectedGroupsPlusUnauthenticated := append([]string{}, env.SupervisorUpstreamOIDC.ExpectedGroups...) - expectedGroupsPlusUnauthenticated = append(expectedGroupsPlusUnauthenticated, "system:authenticated") + expectedGroupsPlusAuthenticated := append([]string{}, env.SupervisorUpstreamOIDC.ExpectedGroups...) + expectedGroupsPlusAuthenticated = append(expectedGroupsPlusAuthenticated, "system:authenticated") // Validate that `pinniped whoami` returns the correct identity. assertWhoami( ctx, From e25eb054502def3b6fb10fcd53028b12196541c3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 11 May 2021 10:31:33 -0700 Subject: [PATCH 43/59] Move Supervisor IDP discovery to its own new endpoint --- cmd/pinniped/cmd/kubeconfig.go | 121 ++++-- cmd/pinniped/cmd/kubeconfig_test.go | 369 ++++++++++++++---- internal/oidc/discovery/discovery_handler.go | 64 +-- .../oidc/discovery/discovery_handler_test.go | 85 +--- .../idpdiscovery/idp_discovery_handler.go | 75 ++++ .../idp_discovery_handler_test.go | 126 ++++++ internal/oidc/oidc.go | 1 + internal/oidc/provider/manager/manager.go | 5 +- .../oidc/provider/manager/manager_test.go | 45 ++- 9 files changed, 660 insertions(+), 231 deletions(-) create mode 100644 internal/oidc/idpdiscovery/idp_discovery_handler.go create mode 100644 internal/oidc/idpdiscovery/idp_discovery_handler_test.go diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index d59b61ba..832b1365 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -95,8 +95,12 @@ type getKubeconfigParams struct { credentialCachePathSet bool } -type supervisorDiscoveryResponse struct { - PinnipedIDPs []pinnipedIDPResponse `json:"pinniped_idps"` +type supervisorOIDCDiscoveryResponse struct { + PinnipedIDPsEndpoint string `json:"pinniped_identity_providers_endpoint"` +} + +type supervisorIDPsDiscoveryResponse struct { + PinnipedIDPs []pinnipedIDPResponse `json:"pinniped_identity_providers"` } type pinnipedIDPResponse struct { @@ -727,57 +731,38 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool } func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams) error { - issuerDiscoveryURL := flags.oidc.issuer + "/.well-known/openid-configuration" - request, err := http.NewRequestWithContext(ctx, http.MethodGet, issuerDiscoveryURL, nil) - if err != nil { - return fmt.Errorf("while forming request to issuer URL: %w", err) - } - transport := &http.Transport{ TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, Proxy: http.ProxyFromEnvironment, } - httpClient := http.Client{Transport: transport} + httpClient := &http.Client{Transport: transport} if flags.oidc.caBundle != nil { rootCAs := x509.NewCertPool() ok := rootCAs.AppendCertsFromPEM(flags.oidc.caBundle) if !ok { - return fmt.Errorf("unable to fetch discovery data from issuer: could not parse CA bundle") + return fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not parse CA bundle") } transport.TLSClientConfig.RootCAs = rootCAs } - response, err := httpClient.Do(request) + pinnipedIDPsEndpoint, err := discoverIDPsDiscoveryEndpointURL(ctx, flags.oidc.issuer, httpClient) if err != nil { - return fmt.Errorf("unable to fetch discovery data from issuer: %w", err) + return err } - defer func() { - _ = response.Body.Close() - }() - if response.StatusCode == http.StatusNotFound { - // 404 Not Found is not an error because OIDC discovery is an optional part of the OIDC spec. + if pinnipedIDPsEndpoint == "" { + // The issuer is not advertising itself as a Pinniped Supervisor which supports upstream IDP discovery. return nil } - if response.StatusCode != http.StatusOK { - // Other types of error responses aside from 404 are not expected. - return fmt.Errorf("unable to fetch discovery data from issuer: unexpected http response status: %s", response.Status) - } - rawBody, err := ioutil.ReadAll(response.Body) + upstreamIDPs, err := discoverAllAvailableSupervisorUpstreamIDPs(ctx, pinnipedIDPsEndpoint, httpClient) if err != nil { - return fmt.Errorf("unable to fetch discovery data from issuer: could not read response body: %w", err) + return err } - var body supervisorDiscoveryResponse - err = json.Unmarshal(rawBody, &body) - if err != nil { - return fmt.Errorf("unable to fetch discovery data from issuer: could not parse response JSON: %w", err) - } - - if len(body.PinnipedIDPs) == 1 { - flags.oidc.upstreamIDPName = body.PinnipedIDPs[0].Name - flags.oidc.upstreamIDPType = body.PinnipedIDPs[0].Type - } else if len(body.PinnipedIDPs) > 1 { - idpName, idpType, err := selectUpstreamIDP(body.PinnipedIDPs, flags.oidc.upstreamIDPName, flags.oidc.upstreamIDPType) + if len(upstreamIDPs) == 1 { + flags.oidc.upstreamIDPName = upstreamIDPs[0].Name + flags.oidc.upstreamIDPType = upstreamIDPs[0].Type + } else if len(upstreamIDPs) > 1 { + idpName, idpType, err := selectUpstreamIDP(upstreamIDPs, flags.oidc.upstreamIDPName, flags.oidc.upstreamIDPType) if err != nil { return err } @@ -787,6 +772,74 @@ func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigPara return nil } +func discoverIDPsDiscoveryEndpointURL(ctx context.Context, issuer string, httpClient *http.Client) (string, error) { + issuerDiscoveryURL := issuer + "/.well-known/openid-configuration" + request, err := http.NewRequestWithContext(ctx, http.MethodGet, issuerDiscoveryURL, nil) + if err != nil { + return "", fmt.Errorf("while forming request to issuer URL: %w", err) + } + + response, err := httpClient.Do(request) + if err != nil { + return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: %w", err) + } + defer func() { + _ = response.Body.Close() + }() + if response.StatusCode == http.StatusNotFound { + // 404 Not Found is not an error because OIDC discovery is an optional part of the OIDC spec. + return "", nil + } + if response.StatusCode != http.StatusOK { + // Other types of error responses aside from 404 are not expected. + return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: unexpected http response status: %s", response.Status) + } + + rawBody, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not read response body: %w", err) + } + + var body supervisorOIDCDiscoveryResponse + err = json.Unmarshal(rawBody, &body) + if err != nil { + return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not parse response JSON: %w", err) + } + + return body.PinnipedIDPsEndpoint, nil +} + +func discoverAllAvailableSupervisorUpstreamIDPs(ctx context.Context, pinnipedIDPsEndpoint string, httpClient *http.Client) ([]pinnipedIDPResponse, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodGet, pinnipedIDPsEndpoint, nil) + if err != nil { + return nil, fmt.Errorf("while forming request to IDP discovery URL: %w", err) + } + + response, err := httpClient.Do(request) + if err != nil { + return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: %w", err) + } + defer func() { + _ = response.Body.Close() + }() + if response.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: unexpected http response status: %s", response.Status) + } + + rawBody, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: could not read response body: %w", err) + } + + var body supervisorIDPsDiscoveryResponse + err = json.Unmarshal(rawBody, &body) + if err != nil { + return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: could not parse response JSON: %w", err) + } + + return body.PinnipedIDPs, nil +} + func selectUpstreamIDP(pinnipedIDPs []pinnipedIDPResponse, idpName, idpType string) (string, string, error) { pinnipedIDPsString, _ := json.Marshal(pinnipedIDPs) switch { diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index e244e14b..c2223350 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -75,21 +75,23 @@ func TestGetKubeconfig(t *testing.T) { } tests := []struct { - name string - args func(string, string) []string - env map[string]string - getPathToSelfErr error - getClientsetErr error - conciergeObjects func(string, string) []runtime.Object - conciergeReactions []kubetesting.Reactor - discoveryResponse string - discoveryStatusCode int - wantLogs func(string, string) []string - wantError bool - wantStdout func(string, string) string - wantStderr func(string, string) string - wantOptionsCount int - wantAPIGroupSuffix string + name string + args func(string, string) []string + env map[string]string + getPathToSelfErr error + getClientsetErr error + conciergeObjects func(string, string) []runtime.Object + conciergeReactions []kubetesting.Reactor + oidcDiscoveryResponse func(string) string + oidcDiscoveryStatusCode int + idpsDiscoveryResponse string + idpsDiscoveryStatusCode int + wantLogs func(string, string) []string + wantError bool + wantStdout func(string, string) string + wantStderr func(string, string) string + wantOptionsCount int + wantAPIGroupSuffix string }{ { name: "help flag passed", @@ -690,7 +692,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, { - name: "when discovery document 400s", + name: "when OIDC discovery document 400s", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -703,7 +705,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryStatusCode: http.StatusBadRequest, + oidcDiscoveryStatusCode: http.StatusBadRequest, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, @@ -718,11 +720,11 @@ func TestGetKubeconfig(t *testing.T) { }, wantError: true, wantStderr: func(issuerCABundle string, issuerURL string) string { - return "Error: unable to fetch discovery data from issuer: unexpected http response status: 400 Bad Request\n" + return "Error: unable to fetch OIDC discovery data from issuer: unexpected http response status: 400 Bad Request\n" }, }, { - name: "when discovery document contains multiple pinniped_idps and no name or type flags are given", + name: "when IDP discovery document returns any error", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -735,9 +737,46 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryStatusCode: http.StatusOK, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [ + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryStatusCode: http.StatusBadRequest, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return "Error: unable to fetch IDP discovery data from issuer: unexpected http response status: 400 Bad Request\n" + }, + }, + { + name: "when IDP discovery document contains multiple pinniped_idps and no name or type flags are given", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, {"name": "some-oidc-idp", "type": "oidc"} ] @@ -762,7 +801,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, { - name: "when discovery document is not valid JSON", + name: "when OIDC discovery document is not valid JSON", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -775,8 +814,9 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryStatusCode: http.StatusOK, - discoveryResponse: "this is not valid JSON", + oidcDiscoveryResponse: func(issuerURL string) string { + return "this is not valid JSON" + }, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, @@ -791,11 +831,46 @@ func TestGetKubeconfig(t *testing.T) { }, wantError: true, wantStderr: func(issuerCABundle string, issuerURL string) string { - return "Error: unable to fetch discovery data from issuer: could not parse response JSON: invalid character 'h' in literal true (expecting 'r')\n" + return "Error: unable to fetch OIDC discovery data from issuer: could not parse response JSON: invalid character 'h' in literal true (expecting 'r')\n" }, }, { - name: "when tls information is missing from jwtauthenticator, test fails because discovery fails", + name: "when IDP discovery document is not valid JSON", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: "this is not valid JSON", + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return "Error: unable to fetch IDP discovery data from issuer: could not parse response JSON: invalid character 'h' in literal true (expecting 'r')\n" + }, + }, + { + name: "when tls information is missing from jwtauthenticator, errors because OIDC discovery fails", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -827,7 +902,7 @@ func TestGetKubeconfig(t *testing.T) { }, wantError: true, wantStderr: func(issuerCABundle string, issuerURL string) string { - return fmt.Sprintf("Error: unable to fetch discovery data from issuer: Get \"%s/.well-known/openid-configuration\": x509: certificate signed by unknown authority\n", issuerURL) + return fmt.Sprintf("Error: unable to fetch OIDC discovery data from issuer: Get \"%s/.well-known/openid-configuration\": x509: certificate signed by unknown authority\n", issuerURL) }, }, { @@ -868,6 +943,40 @@ func TestGetKubeconfig(t *testing.T) { return `Error: while forming request to issuer URL: parse "https%://bad-issuer-url/.well-known/openid-configuration": first path segment in URL cannot contain colon` + "\n" }, }, + { + name: "when the IDP discovery url is bad", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + oidcDiscoveryResponse: func(issuerURL string) string { + return `{"pinniped_identity_providers_endpoint": "https%://illegal_url"}` + }, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return `Error: while forming request to IDP discovery URL: parse "https%://illegal_url": first path segment in URL cannot contain colon` + "\n" + }, + }, { name: "supervisor upstream IDP discovery fails to resolve ambiguity when type is specified but name is not", args: func(issuerCABundle string, issuerURL string) []string { @@ -881,8 +990,11 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [ + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, {"name": "some-other-ldap-idp", "type": "ldap"}, {"name": "some-oidc-idp", "type": "oidc"}, @@ -909,8 +1021,11 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "my-idp", } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [ + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ {"name": "my-idp", "type": "ldap"}, {"name": "my-idp", "type": "oidc"}, {"name": "some-other-oidc-idp", "type": "oidc"} @@ -936,8 +1051,11 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [ + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ {"name": "some-oidc-idp", "type": "oidc"}, {"name": "some-other-oidc-idp", "type": "oidc"} ] @@ -961,8 +1079,11 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "my-nonexistent-idp", } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [ + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ {"name": "some-oidc-idp", "type": "oidc"}, {"name": "some-other-oidc-idp", "type": "oidc"} ] @@ -1464,7 +1585,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, { - name: "Find LDAP idp in discovery document, output ldap related flags", + name: "Find LDAP IDP in IDP discovery document, output ldap related flags", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -1477,8 +1598,13 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [{"name": "some-ldap-idp", "type": "ldap"}] + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ + {"name": "some-ldap-idp", "type": "ldap"} + ] }`), wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -1538,7 +1664,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, { - name: "Find OIDC idp in discovery document, output oidc related flags", + name: "Find OIDC IDP in IDP discovery document, output oidc related flags", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -1551,8 +1677,13 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [{"name": "some-oidc-idp", "type": "oidc"}] + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ + {"name": "some-oidc-idp", "type": "oidc"} + ] }`), wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -1612,7 +1743,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, { - name: "empty idp list in discovery document", + name: "empty IDP list in IDP discovery document", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -1625,8 +1756,11 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [] + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [] }`), wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -1684,7 +1818,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, { - name: "when discovery document 404s, dont set idp related flags", + name: "IDP discovery endpoint is not listed in OIDC discovery document", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -1697,7 +1831,80 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryStatusCode: http.StatusNotFound, + oidcDiscoveryResponse: func(issuerURL string) string { + return `{"other_field": "other_value"}` + }, + idpsDiscoveryStatusCode: http.StatusBadRequest, // IDPs endpoint shouldn't be called by this test + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, + { + name: "when OIDC discovery document 404s, dont set idp related flags", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + oidcDiscoveryStatusCode: http.StatusNotFound, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, @@ -1769,7 +1976,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryStatusCode: http.StatusNotFound, + oidcDiscoveryStatusCode: http.StatusNotFound, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, @@ -1828,7 +2035,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, { - name: "when upstream idp related flags are sent, pass them through even when discovery shows a different idp", + name: "when upstream IDP related flags are sent, pass them through even when IDP discovery shows a different IDP", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -1843,8 +2050,13 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [{"name": "some-other-ldap-idp", "type": "ldap"}] + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ + {"name": "some-other-ldap-idp", "type": "ldap"} + ] }`), wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -1915,8 +2127,13 @@ func TestGetKubeconfig(t *testing.T) { "--oidc-ca-bundle", f.Name(), } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [{"name": "some-ldap-idp", "type": "ldap"}] + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ + {"name": "some-ldap-idp", "type": "ldap"} + ] }`), wantStdout: func(issuerCABundle string, issuerURL string) string { return here.Docf(` @@ -1969,8 +2186,11 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [ + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, {"name": "some-oidc-idp", "type": "oidc"}, {"name": "some-other-oidc-idp", "type": "oidc"} @@ -2027,8 +2247,11 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "some-ldap-idp", } }, - discoveryResponse: here.Docf(`{ - "pinniped_idps": [ + oidcDiscoveryResponse: func(issuerURL string) string { + return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) + }, + idpsDiscoveryResponse: here.Docf(`{ + "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, {"name": "some-oidc-idp", "type": "oidc"}, {"name": "some-other-oidc-idp", "type": "oidc"} @@ -2076,22 +2299,36 @@ func TestGetKubeconfig(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { + var issuerEndpointPtr *string issuerCABundle, issuerEndpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/.well-known/openid-configuration" { - jsonResponseBody := tt.discoveryResponse - if tt.discoveryResponse == "" { - jsonResponseBody = "{}" + switch r.URL.Path { + case "/.well-known/openid-configuration": + jsonResponseBody := "{}" + if tt.oidcDiscoveryResponse != nil { + jsonResponseBody = tt.oidcDiscoveryResponse(*issuerEndpointPtr) } - if tt.discoveryStatusCode == 0 { - tt.discoveryStatusCode = http.StatusOK + if tt.oidcDiscoveryStatusCode == 0 { + tt.oidcDiscoveryStatusCode = http.StatusOK } - w.WriteHeader(tt.discoveryStatusCode) + w.WriteHeader(tt.oidcDiscoveryStatusCode) _, err = w.Write([]byte(jsonResponseBody)) require.NoError(t, err) - } else { - t.Fatalf("tried to call issuer at a path that wasn't the discovery endpoint.") + case "/pinniped_identity_providers": + jsonResponseBody := tt.idpsDiscoveryResponse + if tt.idpsDiscoveryResponse == "" { + jsonResponseBody = "{}" + } + if tt.idpsDiscoveryStatusCode == 0 { + tt.idpsDiscoveryStatusCode = http.StatusOK + } + w.WriteHeader(tt.idpsDiscoveryStatusCode) + _, err = w.Write([]byte(jsonResponseBody)) + require.NoError(t, err) + default: + t.Fatalf("tried to call issuer at a path that wasn't one of the expected discovery endpoints.") } }) + issuerEndpointPtr = &issuerEndpoint testLog := testlogger.New(t) cmd := kubeconfigCommand(kubeconfigDeps{ diff --git a/internal/oidc/discovery/discovery_handler.go b/internal/oidc/discovery/discovery_handler.go index dabdda02..542ef729 100644 --- a/internal/oidc/discovery/discovery_handler.go +++ b/internal/oidc/discovery/discovery_handler.go @@ -8,16 +8,10 @@ import ( "bytes" "encoding/json" "net/http" - "sort" "go.pinniped.dev/internal/oidc" ) -const ( - idpDiscoveryTypeLDAP = "ldap" - idpDiscoveryTypeOIDC = "oidc" -) - // Metadata holds all fields (that we care about) from the OpenID Provider Metadata section in the // OpenID Connect Discovery specification: // https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3. @@ -46,7 +40,7 @@ type Metadata struct { // vvv Custom vvv - IDPs []IdentityProviderMetadata `json:"pinniped_idps"` + PinnipedIDPsEndpoint string `json:"pinniped_identity_providers_endpoint"` // ^^^ Custom ^^^ } @@ -57,14 +51,31 @@ type IdentityProviderMetadata struct { } // NewHandler returns an http.Handler that serves an OIDC discovery endpoint. -func NewHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister) http.Handler { +func NewHandler(issuerURL string) http.Handler { + oidcConfig := Metadata{ + Issuer: issuerURL, + AuthorizationEndpoint: issuerURL + oidc.AuthorizationEndpointPath, + TokenEndpoint: issuerURL + oidc.TokenEndpointPath, + JWKSURI: issuerURL + oidc.JWKSEndpointPath, + PinnipedIDPsEndpoint: issuerURL + oidc.PinnipedIDPsPath, + ResponseTypesSupported: []string{"code"}, + SubjectTypesSupported: []string{"public"}, + IDTokenSigningAlgValuesSupported: []string{"ES256"}, + TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, + ScopesSupported: []string{"openid", "offline"}, + ClaimsSupported: []string{"groups"}, + } + + var b bytes.Buffer + encodeErr := json.NewEncoder(&b).Encode(&oidcConfig) + encodedMetadata := b.Bytes() + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, `Method not allowed (try GET)`, http.StatusMethodNotAllowed) return } - encodedMetadata, encodeErr := metadata(issuerURL, upstreamIDPs) if encodeErr != nil { http.Error(w, encodeErr.Error(), http.StatusInternalServerError) return @@ -77,38 +88,3 @@ func NewHandler(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLis } }) } - -func metadata(issuerURL string, upstreamIDPs oidc.UpstreamIdentityProvidersLister) ([]byte, error) { - oidcConfig := Metadata{ - Issuer: issuerURL, - AuthorizationEndpoint: issuerURL + oidc.AuthorizationEndpointPath, - TokenEndpoint: issuerURL + oidc.TokenEndpointPath, - JWKSURI: issuerURL + oidc.JWKSEndpointPath, - ResponseTypesSupported: []string{"code"}, - SubjectTypesSupported: []string{"public"}, - IDTokenSigningAlgValuesSupported: []string{"ES256"}, - TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, - ScopesSupported: []string{"openid", "offline"}, - ClaimsSupported: []string{"groups"}, - IDPs: []IdentityProviderMetadata{}, - } - - // The cache of IDPs could change at any time, so always recalculate the list. - for _, provider := range upstreamIDPs.GetLDAPIdentityProviders() { - oidcConfig.IDPs = append(oidcConfig.IDPs, IdentityProviderMetadata{Name: provider.GetName(), Type: idpDiscoveryTypeLDAP}) - } - for _, provider := range upstreamIDPs.GetOIDCIdentityProviders() { - oidcConfig.IDPs = append(oidcConfig.IDPs, IdentityProviderMetadata{Name: provider.GetName(), Type: idpDiscoveryTypeOIDC}) - } - - // Nobody like an API that changes the results unnecessarily. :) - sort.SliceStable(oidcConfig.IDPs, func(i, j int) bool { - return oidcConfig.IDPs[i].Name < oidcConfig.IDPs[j].Name - }) - - var b bytes.Buffer - encodeErr := json.NewEncoder(&b).Encode(&oidcConfig) - encodedMetadata := b.Bytes() - - return encodedMetadata, encodeErr -} diff --git a/internal/oidc/discovery/discovery_handler_test.go b/internal/oidc/discovery/discovery_handler_test.go index 7236e544..fd37341b 100644 --- a/internal/oidc/discovery/discovery_handler_test.go +++ b/internal/oidc/discovery/discovery_handler_test.go @@ -9,10 +9,6 @@ import ( "net/http/httptest" "testing" - "go.pinniped.dev/internal/oidc/provider" - - "go.pinniped.dev/internal/testutil/oidctestutil" - "github.com/stretchr/testify/require" "go.pinniped.dev/internal/oidc" @@ -26,11 +22,10 @@ func TestDiscovery(t *testing.T) { method string path string - wantStatus int - wantContentType string - wantFirstResponseBodyJSON interface{} - wantSecondResponseBodyJSON interface{} - wantBodyString string + wantStatus int + wantContentType string + wantBodyJSON interface{} + wantBodyString string }{ { name: "happy path", @@ -39,43 +34,18 @@ func TestDiscovery(t *testing.T) { path: "/some/path" + oidc.WellKnownEndpointPath, wantStatus: http.StatusOK, wantContentType: "application/json", - wantFirstResponseBodyJSON: &Metadata{ + wantBodyJSON: &Metadata{ Issuer: "https://some-issuer.com/some/path", AuthorizationEndpoint: "https://some-issuer.com/some/path/oauth2/authorize", TokenEndpoint: "https://some-issuer.com/some/path/oauth2/token", JWKSURI: "https://some-issuer.com/some/path/jwks.json", + PinnipedIDPsEndpoint: "https://some-issuer.com/some/path/pinniped_identity_providers", ResponseTypesSupported: []string{"code"}, SubjectTypesSupported: []string{"public"}, IDTokenSigningAlgValuesSupported: []string{"ES256"}, TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, ScopesSupported: []string{"openid", "offline"}, ClaimsSupported: []string{"groups"}, - IDPs: []IdentityProviderMetadata{ - {Name: "a-some-ldap-idp", Type: "ldap"}, - {Name: "a-some-oidc-idp", Type: "oidc"}, - {Name: "x-some-idp", Type: "ldap"}, - {Name: "x-some-idp", Type: "oidc"}, - {Name: "z-some-ldap-idp", Type: "ldap"}, - {Name: "z-some-oidc-idp", Type: "oidc"}, - }, - }, - wantSecondResponseBodyJSON: &Metadata{ - Issuer: "https://some-issuer.com/some/path", - AuthorizationEndpoint: "https://some-issuer.com/some/path/oauth2/authorize", - TokenEndpoint: "https://some-issuer.com/some/path/oauth2/token", - JWKSURI: "https://some-issuer.com/some/path/jwks.json", - ResponseTypesSupported: []string{"code"}, - SubjectTypesSupported: []string{"public"}, - IDTokenSigningAlgValuesSupported: []string{"ES256"}, - TokenEndpointAuthMethodsSupported: []string{"client_secret_basic"}, - ScopesSupported: []string{"openid", "offline"}, - ClaimsSupported: []string{"groups"}, - IDPs: []IdentityProviderMetadata{ - {Name: "some-other-ldap-idp-1", Type: "ldap"}, - {Name: "some-other-ldap-idp-2", Type: "ldap"}, - {Name: "some-other-oidc-idp-1", Type: "oidc"}, - {Name: "some-other-oidc-idp-2", Type: "oidc"}, - }, }, }, { @@ -91,16 +61,7 @@ func TestDiscovery(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - idpLister := oidctestutil.NewUpstreamIDPListerBuilder(). - WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "z-some-oidc-idp"}). - WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "x-some-idp"}). - WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "a-some-ldap-idp"}). - WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "a-some-oidc-idp"}). - WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ldap-idp"}). - WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "x-some-idp"}). - Build() - - handler := NewHandler(test.issuer, idpLister) + handler := NewHandler(test.issuer) req := httptest.NewRequest(test.method, test.path, nil) rsp := httptest.NewRecorder() handler.ServeHTTP(rsp, req) @@ -109,36 +70,8 @@ func TestDiscovery(t *testing.T) { require.Equal(t, test.wantContentType, rsp.Header().Get("Content-Type")) - if test.wantFirstResponseBodyJSON != nil { - wantJSON, err := json.Marshal(test.wantFirstResponseBodyJSON) - require.NoError(t, err) - require.JSONEq(t, string(wantJSON), rsp.Body.String()) - } - - if test.wantBodyString != "" { - require.Equal(t, test.wantBodyString, rsp.Body.String()) - } - - // Change the list of IDPs in the cache. - idpLister.SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ - &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-1"}, - &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-2"}, - }) - idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ - &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-1"}, - &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-2"}, - }) - - // Make the same request to the same handler instance again, and expect different results. - rsp = httptest.NewRecorder() - handler.ServeHTTP(rsp, req) - - require.Equal(t, test.wantStatus, rsp.Code) - - require.Equal(t, test.wantContentType, rsp.Header().Get("Content-Type")) - - if test.wantFirstResponseBodyJSON != nil { - wantJSON, err := json.Marshal(test.wantSecondResponseBodyJSON) + if test.wantBodyJSON != nil { + wantJSON, err := json.Marshal(test.wantBodyJSON) require.NoError(t, err) require.JSONEq(t, string(wantJSON), rsp.Body.String()) } diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler.go b/internal/oidc/idpdiscovery/idp_discovery_handler.go new file mode 100644 index 00000000..9ee0bf76 --- /dev/null +++ b/internal/oidc/idpdiscovery/idp_discovery_handler.go @@ -0,0 +1,75 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package idpdiscovery provides a handler for the upstream IDP discovery endpoint. +package idpdiscovery + +import ( + "bytes" + "encoding/json" + "net/http" + "sort" + + "go.pinniped.dev/internal/oidc" +) + +const ( + idpDiscoveryTypeLDAP = "ldap" + idpDiscoveryTypeOIDC = "oidc" +) + +type response struct { + IDPs []identityProviderResponse `json:"pinniped_identity_providers"` +} + +type identityProviderResponse struct { + Name string `json:"name"` + Type string `json:"type"` +} + +// NewHandler returns an http.Handler that serves the upstream IDP discovery endpoint. +func NewHandler(upstreamIDPs oidc.UpstreamIdentityProvidersLister) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, `Method not allowed (try GET)`, http.StatusMethodNotAllowed) + return + } + + encodedMetadata, encodeErr := responseAsJSON(upstreamIDPs) + if encodeErr != nil { + http.Error(w, encodeErr.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(encodedMetadata); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + }) +} + +func responseAsJSON(upstreamIDPs oidc.UpstreamIdentityProvidersLister) ([]byte, error) { + r := response{ + IDPs: []identityProviderResponse{}, + } + + // The cache of IDPs could change at any time, so always recalculate the list. + for _, provider := range upstreamIDPs.GetLDAPIdentityProviders() { + r.IDPs = append(r.IDPs, identityProviderResponse{Name: provider.GetName(), Type: idpDiscoveryTypeLDAP}) + } + for _, provider := range upstreamIDPs.GetOIDCIdentityProviders() { + r.IDPs = append(r.IDPs, identityProviderResponse{Name: provider.GetName(), Type: idpDiscoveryTypeOIDC}) + } + + // Nobody like an API that changes the results unnecessarily. :) + sort.SliceStable(r.IDPs, func(i, j int) bool { + return r.IDPs[i].Name < r.IDPs[j].Name + }) + + var b bytes.Buffer + encodeErr := json.NewEncoder(&b).Encode(&r) + encodedMetadata := b.Bytes() + + return encodedMetadata, encodeErr +} diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go new file mode 100644 index 00000000..3912f9c9 --- /dev/null +++ b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go @@ -0,0 +1,126 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package idpdiscovery + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "go.pinniped.dev/internal/oidc" + "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/testutil/oidctestutil" +) + +func TestIDPDiscovery(t *testing.T) { + tests := []struct { + name string + + method string + path string + + wantStatus int + wantContentType string + wantFirstResponseBodyJSON interface{} + wantSecondResponseBodyJSON interface{} + wantBodyString string + }{ + { + name: "happy path", + method: http.MethodGet, + path: "/some/path" + oidc.WellKnownEndpointPath, + wantStatus: http.StatusOK, + wantContentType: "application/json", + wantFirstResponseBodyJSON: &response{ + IDPs: []identityProviderResponse{ + {Name: "a-some-ldap-idp", Type: "ldap"}, + {Name: "a-some-oidc-idp", Type: "oidc"}, + {Name: "x-some-idp", Type: "ldap"}, + {Name: "x-some-idp", Type: "oidc"}, + {Name: "z-some-ldap-idp", Type: "ldap"}, + {Name: "z-some-oidc-idp", Type: "oidc"}, + }, + }, + wantSecondResponseBodyJSON: &response{ + IDPs: []identityProviderResponse{ + {Name: "some-other-ldap-idp-1", Type: "ldap"}, + {Name: "some-other-ldap-idp-2", Type: "ldap"}, + {Name: "some-other-oidc-idp-1", Type: "oidc"}, + {Name: "some-other-oidc-idp-2", Type: "oidc"}, + }, + }, + }, + { + name: "bad method", + method: http.MethodPost, + path: oidc.WellKnownEndpointPath, + wantStatus: http.StatusMethodNotAllowed, + wantContentType: "text/plain; charset=utf-8", + wantBodyString: "Method not allowed (try GET)\n", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + idpLister := oidctestutil.NewUpstreamIDPListerBuilder(). + WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "z-some-oidc-idp"}). + WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "x-some-idp"}). + WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "a-some-ldap-idp"}). + WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "a-some-oidc-idp"}). + WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ldap-idp"}). + WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "x-some-idp"}). + Build() + + handler := NewHandler(idpLister) + req := httptest.NewRequest(test.method, test.path, nil) + rsp := httptest.NewRecorder() + handler.ServeHTTP(rsp, req) + + require.Equal(t, test.wantStatus, rsp.Code) + + require.Equal(t, test.wantContentType, rsp.Header().Get("Content-Type")) + + if test.wantFirstResponseBodyJSON != nil { + wantJSON, err := json.Marshal(test.wantFirstResponseBodyJSON) + require.NoError(t, err) + require.JSONEq(t, string(wantJSON), rsp.Body.String()) + } + + if test.wantBodyString != "" { + require.Equal(t, test.wantBodyString, rsp.Body.String()) + } + + // Change the list of IDPs in the cache. + idpLister.SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-1"}, + &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ldap-idp-2"}, + }) + idpLister.SetOIDCIdentityProviders([]provider.UpstreamOIDCIdentityProviderI{ + &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-1"}, + &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-2"}, + }) + + // Make the same request to the same handler instance again, and expect different results. + rsp = httptest.NewRecorder() + handler.ServeHTTP(rsp, req) + + require.Equal(t, test.wantStatus, rsp.Code) + + require.Equal(t, test.wantContentType, rsp.Header().Get("Content-Type")) + + if test.wantFirstResponseBodyJSON != nil { + wantJSON, err := json.Marshal(test.wantSecondResponseBodyJSON) + require.NoError(t, err) + require.JSONEq(t, string(wantJSON), rsp.Body.String()) + } + + if test.wantBodyString != "" { + require.Equal(t, test.wantBodyString, rsp.Body.String()) + } + }) + } +} diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 0297b43c..82fe511d 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -24,6 +24,7 @@ const ( TokenEndpointPath = "/oauth2/token" //nolint:gosec // ignore lint warning that this is a credential CallbackEndpointPath = "/callback" JWKSEndpointPath = "/jwks.json" + PinnipedIDPsPath = "/pinniped_identity_providers" ) const ( diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index e17a4737..1d41e4ef 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -16,6 +16,7 @@ import ( "go.pinniped.dev/internal/oidc/csrftoken" "go.pinniped.dev/internal/oidc/discovery" "go.pinniped.dev/internal/oidc/dynamiccodec" + "go.pinniped.dev/internal/oidc/idpdiscovery" "go.pinniped.dev/internal/oidc/jwks" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/oidc/token" @@ -102,10 +103,12 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs wrapGetter(incomingProvider.Issuer(), m.secretCache.GetStateEncoderBlockKey), ) - m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer, m.upstreamIDPs) + m.providerHandlers[(issuerHostWithPath + oidc.WellKnownEndpointPath)] = discovery.NewHandler(issuer) m.providerHandlers[(issuerHostWithPath + oidc.JWKSEndpointPath)] = jwks.NewHandler(issuer, m.dynamicJWKSProvider) + m.providerHandlers[(issuerHostWithPath + oidc.PinnipedIDPsPath)] = idpdiscovery.NewHandler(m.upstreamIDPs) + m.providerHandlers[(issuerHostWithPath + oidc.AuthorizationEndpointPath)] = auth.NewHandler( issuer, m.upstreamIDPs, diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index f42fe11c..c99fadff 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -7,6 +7,7 @@ import ( "context" "crypto/ecdsa" "encoding/json" + "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -70,7 +71,7 @@ func TestManager(t *testing.T) { return req } - requireDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIssuer, expectedIDPName, expectedIDPType string) { + requireDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIssuer string) { recorder := httptest.NewRecorder() subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.WellKnownEndpointPath+requestURLSuffix)) @@ -85,9 +86,24 @@ func TestManager(t *testing.T) { err = json.Unmarshal(responseBody, &parsedDiscoveryResult) r.NoError(err) r.Equal(expectedIssuer, parsedDiscoveryResult.Issuer) - r.Len(parsedDiscoveryResult.IDPs, 1) - r.Equal(expectedIDPName, parsedDiscoveryResult.IDPs[0].Name) - r.Equal(expectedIDPType, parsedDiscoveryResult.IDPs[0].Type) + r.Equal(parsedDiscoveryResult.PinnipedIDPsEndpoint, expectedIssuer+oidc.PinnipedIDPsPath) + } + + requirePinnipedIDPsDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIDPName, expectedIDPType string) { + recorder := httptest.NewRecorder() + + subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.PinnipedIDPsPath+requestURLSuffix)) + + r.False(fallbackHandlerWasCalled) + + // Minimal check to ensure that the right IDP discovery endpoint was called + r.Equal(http.StatusOK, recorder.Code) + responseBody, err := ioutil.ReadAll(recorder.Body) + r.NoError(err) + r.Equal( + fmt.Sprintf(`{"pinniped_identity_providers":[{"name":"%s","type":"%s"}]}`+"\n", expectedIDPName, expectedIDPType), + string(responseBody), + ) } requireAuthorizationRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedRedirectLocationPrefix string) (string, string) { @@ -289,14 +305,23 @@ func TestManager(t *testing.T) { } requireRoutesMatchingRequestsToAppropriateProvider := func() { - requireDiscoveryRequestToBeHandled(issuer1, "", issuer1, upstreamIDPName, upstreamIDPType) - requireDiscoveryRequestToBeHandled(issuer2, "", issuer2, upstreamIDPName, upstreamIDPType) - requireDiscoveryRequestToBeHandled(issuer2, "?some=query", issuer2, upstreamIDPName, upstreamIDPType) + requireDiscoveryRequestToBeHandled(issuer1, "", issuer1) + requireDiscoveryRequestToBeHandled(issuer2, "", issuer2) + requireDiscoveryRequestToBeHandled(issuer2, "?some=query", issuer2) // Hostnames are case-insensitive, so test that we can handle that. - requireDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", issuer1, upstreamIDPName, upstreamIDPType) - requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", issuer2, upstreamIDPName, upstreamIDPType) - requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2, upstreamIDPName, upstreamIDPType) + requireDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", issuer1) + requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", issuer2) + requireDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", issuer2) + + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer1, "", upstreamIDPName, upstreamIDPType) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2, "", upstreamIDPName, upstreamIDPType) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2, "?some=query", upstreamIDPName, upstreamIDPType) + + // Hostnames are case-insensitive, so test that we can handle that. + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer1DifferentCaseHostname, "", upstreamIDPName, upstreamIDPType) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "", upstreamIDPName, upstreamIDPType) + requirePinnipedIDPsDiscoveryRequestToBeHandled(issuer2DifferentCaseHostname, "?some=query", upstreamIDPName, upstreamIDPType) issuer1JWKS := requireJWKSRequestToBeHandled(issuer1, "", issuer1KeyID) issuer2JWKS := requireJWKSRequestToBeHandled(issuer2, "", issuer2KeyID) From 6723ed9fd83d23cfad47284b93fb65cc92de2ca3 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Tue, 11 May 2021 13:55:46 -0700 Subject: [PATCH 44/59] Add end-to-end integration test for CLI-based LDAP login --- go.mod | 1 + go.sum | 1 + test/integration/e2e_test.go | 534 +++++++++++++++++++++++------------ 3 files changed, 349 insertions(+), 187 deletions(-) diff --git a/go.mod b/go.mod index 96f6e4db..d86141f4 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( cloud.google.com/go v0.60.0 // indirect github.com/MakeNowJust/heredoc/v2 v2.0.1 github.com/coreos/go-oidc/v3 v3.0.0 + github.com/creack/pty v1.1.11 github.com/davecgh/go-spew v1.1.1 github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-logr/logr v0.4.0 diff --git a/go.sum b/go.sum index b5ac283a..e58f11bb 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 2d7731c6..59192796 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "errors" "fmt" + "io" "io/ioutil" "net/url" "os" @@ -21,6 +22,7 @@ import ( "time" coreosoidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/creack/pty" "github.com/stretchr/testify/require" authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" @@ -30,6 +32,7 @@ import ( configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" "go.pinniped.dev/internal/certauthority" + "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient" @@ -92,24 +95,6 @@ func TestE2EFullIntegration(t *testing.T) { configv1alpha1.SuccessFederationDomainStatusCondition, ) - // Create upstream OIDC provider and wait for it to become ready. - library.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ - Issuer: env.SupervisorUpstreamOIDC.Issuer, - TLS: &idpv1alpha1.TLSSpec{ - CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), - }, - AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ - AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, - }, - Claims: idpv1alpha1.OIDCClaims{ - Username: env.SupervisorUpstreamOIDC.UsernameClaim, - Groups: env.SupervisorUpstreamOIDC.GroupsClaim, - }, - Client: idpv1alpha1.OIDCClient{ - SecretName: library.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, - }, - }, idpv1alpha1.PhaseReady) - // Create a JWTAuthenticator that will validate the tokens from the downstream issuer. clusterAudience := "test-cluster-" + library.RandHex(t, 8) authenticator := library.CreateTestJWTAuthenticator(ctx, t, authv1alpha.JWTAuthenticatorSpec{ @@ -118,159 +103,314 @@ func TestE2EFullIntegration(t *testing.T) { TLS: &authv1alpha.TLSSpec{CertificateAuthorityData: testCABundleBase64}, }) - // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. - library.CreateTestClusterRoleBinding(t, - rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: env.SupervisorUpstreamOIDC.Username}, - rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"}, - ) - library.WaitForUserToHaveAccess(t, env.SupervisorUpstreamOIDC.Username, []string{}, &authorizationv1.ResourceAttributes{ - Verb: "get", - Group: "", - Version: "v1", - Resource: "namespaces", + // Add an OIDC upstream IDP and try using it to authenticate during kubectl commands. + t.Run("with Supervisor OIDC upstream IDP", func(t *testing.T) { + expectedUsername := env.SupervisorUpstreamOIDC.Username + expectedGroups := env.SupervisorUpstreamOIDC.ExpectedGroups + + // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. + library.CreateTestClusterRoleBinding(t, + rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: expectedUsername}, + rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"}, + ) + library.WaitForUserToHaveAccess(t, expectedUsername, []string{}, &authorizationv1.ResourceAttributes{ + Verb: "get", + Group: "", + Version: "v1", + Resource: "namespaces", + }) + + // Create upstream OIDC provider and wait for it to become ready. + library.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), + }, + AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ + AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, + }, + Claims: idpv1alpha1.OIDCClaims{ + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, + }, + Client: idpv1alpha1.OIDCClient{ + SecretName: library.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + }, + }, idpv1alpha1.PhaseReady) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/oidc-test-sessions.yaml" + + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ + "get", "kubeconfig", + "--concierge-api-group-suffix", env.APIGroupSuffix, + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", authenticator.Name, + "--oidc-skip-browser", + "--oidc-ca-bundle", testCABundlePath, + "--oidc-session-cache", sessionCachePath, + }) + + // Run "kubectl get namespaces" which should trigger a browser login via the plugin. + start := time.Now() + kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath) + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + stderrPipe, err := kubectlCmd.StderrPipe() + require.NoError(t, err) + stdoutPipe, err := kubectlCmd.StdoutPipe() + require.NoError(t, err) + + t.Logf("starting kubectl subprocess") + require.NoError(t, kubectlCmd.Start()) + t.Cleanup(func() { + err := kubectlCmd.Wait() + t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode()) + stdout, stdoutErr := ioutil.ReadAll(stdoutPipe) + if stdoutErr != nil { + stdout = []byte("") + } + stderr, stderrErr := ioutil.ReadAll(stderrPipe) + if stderrErr != nil { + stderr = []byte("") + } + require.NoErrorf(t, err, "kubectl process did not exit cleanly, stdout/stderr: %q/%q", string(stdout), string(stderr)) + }) + + // Start a background goroutine to read stderr from the CLI and parse out the login URL. + loginURLChan := make(chan string) + spawnTestGoroutine(t, func() (err error) { + defer func() { + closeErr := stderrPipe.Close() + if closeErr == nil || errors.Is(closeErr, os.ErrClosed) { + return + } + if err == nil { + err = fmt.Errorf("stderr stream closed with error: %w", closeErr) + } + }() + + reader := bufio.NewReader(library.NewLoggerReader(t, "stderr", stderrPipe)) + line, err := reader.ReadString('\n') + if err != nil { + return fmt.Errorf("could not read login URL line from stderr: %w", err) + } + const prompt = "Please log in: " + if !strings.HasPrefix(line, prompt) { + return fmt.Errorf("expected %q to have prefix %q", line, prompt) + } + loginURLChan <- strings.TrimPrefix(line, prompt) + return readAndExpectEmpty(reader) + }) + + // Start a background goroutine to read stdout from kubectl and return the result as a string. + kubectlOutputChan := make(chan string) + spawnTestGoroutine(t, func() (err error) { + defer func() { + closeErr := stdoutPipe.Close() + if closeErr == nil || errors.Is(closeErr, os.ErrClosed) { + return + } + if err == nil { + err = fmt.Errorf("stdout stream closed with error: %w", closeErr) + } + }() + output, err := ioutil.ReadAll(stdoutPipe) + if err != nil { + return err + } + t.Logf("kubectl output:\n%s\n", output) + kubectlOutputChan <- string(output) + return nil + }) + + // Wait for the CLI to print out the login URL and open the browser to it. + t.Logf("waiting for CLI to output login URL") + var loginURL string + select { + case <-time.After(1 * time.Minute): + require.Fail(t, "timed out waiting for login URL") + case loginURL = <-loginURLChan: + } + t.Logf("navigating to login page") + require.NoError(t, page.Navigate(loginURL)) + + // Expect to be redirected to the upstream provider and log in. + browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) + + // Expect to be redirected to the localhost callback. + t.Logf("waiting for redirect to callback") + browsertest.WaitForURL(t, page, regexp.MustCompile(`\Ahttp://127\.0\.0\.1:[0-9]+/callback\?.+\z`)) + + // Wait for the "pre" element that gets rendered for a `text/plain` page, and + // assert that it contains the success message. + t.Logf("verifying success page") + browsertest.WaitForVisibleElements(t, page, "pre") + msg, err := page.First("pre").Text() + require.NoError(t, err) + require.Equal(t, "you have been logged in and may now close this tab", msg) + + // Expect the CLI to output a list of namespaces in JSON format. + t.Logf("waiting for kubectl to output namespace list JSON") + var kubectlOutput string + select { + case <-time.After(10 * time.Second): + require.Fail(t, "timed out waiting for kubectl output") + case kubectlOutput = <-kubectlOutputChan: + } + require.Greaterf(t, len(strings.Split(kubectlOutput, "\n")), 2, "expected some namespaces to be returned, got %q", kubectlOutput) + t.Logf("first kubectl command took %s", time.Since(start).String()) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env, + downstream, + kubeconfigPath, + sessionCachePath, + pinnipedExe, + expectedUsername, + expectedGroups, + ) }) - // Use a specific session cache for this test. - sessionCachePath := tempDir + "/sessions.yaml" + // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands. + t.Run("with Supervisor LDAP upstream IDP", func(t *testing.T) { + expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue + expectedGroups := []string{} // LDAP groups are not implemented yet - // Run "pinniped get kubeconfig" to get a kubeconfig YAML. - envVarsWithProxy := append(os.Environ(), env.ProxyEnv()...) - kubeconfigYAML, stderr := runPinnipedCLI(t, envVarsWithProxy, pinnipedExe, "get", "kubeconfig", - "--concierge-api-group-suffix", env.APIGroupSuffix, - "--concierge-authenticator-type", "jwt", - "--concierge-authenticator-name", authenticator.Name, - "--oidc-skip-browser", - "--oidc-ca-bundle", testCABundlePath, - "--oidc-session-cache", sessionCachePath, - ) - t.Logf("stderr output from 'pinniped get kubeconfig':\n%s\n\n", stderr) - t.Logf("test kubeconfig:\n%s\n\n", kubeconfigYAML) + // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. + library.CreateTestClusterRoleBinding(t, + rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: expectedUsername}, + rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"}, + ) + library.WaitForUserToHaveAccess(t, expectedUsername, []string{}, &authorizationv1.ResourceAttributes{ + Verb: "get", + Group: "", + Version: "v1", + Resource: "namespaces", + }) - restConfig := library.NewRestConfigFromKubeconfig(t, kubeconfigYAML) - require.NotNil(t, restConfig.ExecProvider) - require.Equal(t, []string{"login", "oidc"}, restConfig.ExecProvider.Args[:2]) - kubeconfigPath := filepath.Join(tempDir, "kubeconfig.yaml") - require.NoError(t, ioutil.WriteFile(kubeconfigPath, []byte(kubeconfigYAML), 0600)) + // Put the bind service account's info into a Secret. + bindSecret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", corev1.SecretTypeBasicAuth, + map[string]string{ + corev1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, + corev1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, + }, + ) - // Run "kubectl get namespaces" which should trigger a browser login via the plugin. - start := time.Now() + // Create upstream LDAP provider and wait for it to become ready. + library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), + }, + Bind: idpv1alpha1.LDAPIdentityProviderBind{ + SecretName: bindSecret.Name, + }, + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ + Base: env.SupervisorUpstreamLDAP.UserSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ + Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + }, + }, + }, idpv1alpha1.LDAPPhaseReady) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/ldap-test-sessions.yaml" + + kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{ + "get", "kubeconfig", + "--concierge-api-group-suffix", env.APIGroupSuffix, + "--concierge-authenticator-type", "jwt", + "--concierge-authenticator-name", authenticator.Name, + "--oidc-session-cache", sessionCachePath, + }) + + // Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin. + start := time.Now() + kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath) + kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) + ptyFile, err := pty.Start(kubectlCmd) + require.NoError(t, err) + + // Wait for the subprocess to print the username prompt, then type the user's username. + readFromFileUntilStringIsSeen(t, ptyFile, "Username: ") + _, err = ptyFile.WriteString(expectedUsername + "\n") + require.NoError(t, err) + + // Wait for the subprocess to print the password prompt, then type the user's password. + readFromFileUntilStringIsSeen(t, ptyFile, "Password: ") + _, err = ptyFile.WriteString(env.SupervisorUpstreamLDAP.TestUserPassword + "\n") + require.NoError(t, err) + + // Read all of the remaining output from the subprocess until EOF. + remainingOutput, err := ioutil.ReadAll(ptyFile) + require.NoError(t, err) + require.Greaterf(t, len(strings.Split(string(remainingOutput), "\n")), 2, "expected some namespaces to be returned, got %q", string(remainingOutput)) + t.Logf("first kubectl command took %s", time.Since(start).String()) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env, + downstream, + kubeconfigPath, + sessionCachePath, + pinnipedExe, + expectedUsername, + expectedGroups, + ) + }) +} + +func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) string { + readFromFile := "" + + library.RequireEventuallyWithoutError(t, func() (bool, error) { + someOutput, foundEOF := readAvailableOutput(t, f) + readFromFile += someOutput + if strings.Contains(readFromFile, until) { + return true, nil // found it! finished. + } + if foundEOF { + return false, fmt.Errorf("reached EOF of subcommand's output without seeing expected string %q", until) + } + return false, nil // keep waiting and reading + }, 1*time.Minute, 1*time.Second) + + return readFromFile +} + +func readAvailableOutput(t *testing.T, r io.Reader) (string, bool) { + buf := make([]byte, 1024) + n, err := r.Read(buf) + if err != nil { + if err == io.EOF { + return string(buf[:n]), true + } else { + require.NoError(t, err) + } + } + return string(buf[:n]), false +} + +func requireUserCanUseKubectlWithoutAuthenticatingAgain( + ctx context.Context, + t *testing.T, + env *library.TestEnv, + downstream *configv1alpha1.FederationDomain, + kubeconfigPath string, + sessionCachePath string, + pinnipedExe string, + expectedUsername string, + expectedGroups []string, +) { + // Run kubectl, which should work without any prompting for authentication. kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath) kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...) - stderrPipe, err := kubectlCmd.StderrPipe() - require.NoError(t, err) - stdoutPipe, err := kubectlCmd.StdoutPipe() - require.NoError(t, err) - - t.Logf("starting kubectl subprocess") - require.NoError(t, kubectlCmd.Start()) - t.Cleanup(func() { - err := kubectlCmd.Wait() - t.Logf("kubectl subprocess exited with code %d", kubectlCmd.ProcessState.ExitCode()) - stdout, stdoutErr := ioutil.ReadAll(stdoutPipe) - if stdoutErr != nil { - stdout = []byte("") - } - stderr, stderrErr := ioutil.ReadAll(stderrPipe) - if stderrErr != nil { - stderr = []byte("") - } - require.NoErrorf(t, err, "kubectl process did not exit cleanly, stdout/stderr: %q/%q", string(stdout), string(stderr)) - }) - - // Start a background goroutine to read stderr from the CLI and parse out the login URL. - loginURLChan := make(chan string) - spawnTestGoroutine(t, func() (err error) { - defer func() { - closeErr := stderrPipe.Close() - if closeErr == nil || errors.Is(closeErr, os.ErrClosed) { - return - } - if err == nil { - err = fmt.Errorf("stderr stream closed with error: %w", closeErr) - } - }() - - reader := bufio.NewReader(library.NewLoggerReader(t, "stderr", stderrPipe)) - line, err := reader.ReadString('\n') - if err != nil { - return fmt.Errorf("could not read login URL line from stderr: %w", err) - } - const prompt = "Please log in: " - if !strings.HasPrefix(line, prompt) { - return fmt.Errorf("expected %q to have prefix %q", line, prompt) - } - loginURLChan <- strings.TrimPrefix(line, prompt) - return readAndExpectEmpty(reader) - }) - - // Start a background goroutine to read stdout from kubectl and return the result as a string. - kubectlOutputChan := make(chan string) - spawnTestGoroutine(t, func() (err error) { - defer func() { - closeErr := stdoutPipe.Close() - if closeErr == nil || errors.Is(closeErr, os.ErrClosed) { - return - } - if err == nil { - err = fmt.Errorf("stdout stream closed with error: %w", closeErr) - } - }() - output, err := ioutil.ReadAll(stdoutPipe) - if err != nil { - return err - } - t.Logf("kubectl output:\n%s\n", output) - kubectlOutputChan <- string(output) - return nil - }) - - // Wait for the CLI to print out the login URL and open the browser to it. - t.Logf("waiting for CLI to output login URL") - var loginURL string - select { - case <-time.After(1 * time.Minute): - require.Fail(t, "timed out waiting for login URL") - case loginURL = <-loginURLChan: - } - t.Logf("navigating to login page") - require.NoError(t, page.Navigate(loginURL)) - - // Expect to be redirected to the upstream provider and log in. - browsertest.LoginToUpstream(t, page, env.SupervisorUpstreamOIDC) - - // Expect to be redirected to the localhost callback. - t.Logf("waiting for redirect to callback") - browsertest.WaitForURL(t, page, regexp.MustCompile(`\Ahttp://127\.0\.0\.1:[0-9]+/callback\?.+\z`)) - - // Wait for the "pre" element that gets rendered for a `text/plain` page, and - // assert that it contains the success message. - t.Logf("verifying success page") - browsertest.WaitForVisibleElements(t, page, "pre") - msg, err := page.First("pre").Text() - require.NoError(t, err) - require.Equal(t, "you have been logged in and may now close this tab", msg) - - // Expect the CLI to output a list of namespaces in JSON format. - t.Logf("waiting for kubectl to output namespace list JSON") - var kubectlOutput string - select { - case <-time.After(10 * time.Second): - require.Fail(t, "timed out waiting for kubectl output") - case kubectlOutput = <-kubectlOutputChan: - } - require.Greaterf(t, len(strings.Split(kubectlOutput, "\n")), 2, "expected some namespaces to be returned, got %q", kubectlOutput) - t.Logf("first kubectl command took %s", time.Since(start).String()) - - // Run kubectl again, which should work with no browser interaction. - kubectlCmd2 := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath) - kubectlCmd2.Env = append(os.Environ(), env.ProxyEnv()...) - start = time.Now() - kubectlOutput2, err := kubectlCmd2.CombinedOutput() + startTime := time.Now() + kubectlOutput2, err := kubectlCmd.CombinedOutput() require.NoError(t, err) require.Greaterf(t, len(bytes.Split(kubectlOutput2, []byte("\n"))), 2, "expected some namespaces to be returned again") - t.Logf("second kubectl command took %s", time.Since(start).String()) + t.Logf("second kubectl command took %s", time.Since(startTime).String()) - // probe our cache for the current ID token as a proxy for a whoami API + // Probe our cache for the current ID token as a proxy for a whoami API. cache := filesession.New(sessionCachePath, filesession.WithErrorReporter(func(err error) { require.NoError(t, err) })) @@ -286,49 +426,52 @@ func TestE2EFullIntegration(t *testing.T) { require.NotNil(t, token) idTokenClaims := token.IDToken.Claims - require.Equal(t, env.SupervisorUpstreamOIDC.Username, idTokenClaims[oidc.DownstreamUsernameClaim]) + require.Equal(t, expectedUsername, idTokenClaims[oidc.DownstreamUsernameClaim]) // The groups claim in the file ends up as an []interface{}, so adjust our expectation to match. - expectedGroups := make([]interface{}, 0, len(env.SupervisorUpstreamOIDC.ExpectedGroups)) - for _, g := range env.SupervisorUpstreamOIDC.ExpectedGroups { - expectedGroups = append(expectedGroups, g) + expectedGroupsAsEmptyInterfaces := make([]interface{}, 0, len(expectedGroups)) + for _, g := range expectedGroups { + expectedGroupsAsEmptyInterfaces = append(expectedGroupsAsEmptyInterfaces, g) } - require.Equal(t, expectedGroups, idTokenClaims[oidc.DownstreamGroupsClaim]) + require.Equal(t, expectedGroupsAsEmptyInterfaces, idTokenClaims[oidc.DownstreamGroupsClaim]) - // confirm we are the right user according to Kube expectedYAMLGroups := func() string { var b strings.Builder - for _, g := range env.SupervisorUpstreamOIDC.ExpectedGroups { + for _, g := range expectedGroups { b.WriteString("\n") b.WriteString(` - `) b.WriteString(g) } return b.String() }() + + // Confirm we are the right user according to Kube by calling the whoami API. kubectlCmd3 := exec.CommandContext(ctx, "kubectl", "create", "-f", "-", "-o", "yaml", "--kubeconfig", kubeconfigPath) kubectlCmd3.Env = append(os.Environ(), env.ProxyEnv()...) - kubectlCmd3.Stdin = strings.NewReader(` -apiVersion: identity.concierge.` + env.APIGroupSuffix + `/v1alpha1 -kind: WhoAmIRequest -`) + kubectlCmd3.Stdin = strings.NewReader(here.Docf(` + apiVersion: identity.concierge.%s/v1alpha1 + kind: WhoAmIRequest + `, env.APIGroupSuffix)) + kubectlOutput3, err := kubectlCmd3.CombinedOutput() require.NoError(t, err) - require.Equal(t, - `apiVersion: identity.concierge.`+env.APIGroupSuffix+`/v1alpha1 -kind: WhoAmIRequest -metadata: - creationTimestamp: null -spec: {} -status: - kubernetesUserInfo: - user: - groups:`+expectedYAMLGroups+` - - system:authenticated - username: `+env.SupervisorUpstreamOIDC.Username+` -`, + + require.Equal(t, here.Docf(` + apiVersion: identity.concierge.%s/v1alpha1 + kind: WhoAmIRequest + metadata: + creationTimestamp: null + spec: {} + status: + kubernetesUserInfo: + user: + groups:%s + - system:authenticated + username: %s + `, env.APIGroupSuffix, expectedYAMLGroups, expectedUsername), string(kubectlOutput3)) - expectedGroupsPlusAuthenticated := append([]string{}, env.SupervisorUpstreamOIDC.ExpectedGroups...) + expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...) expectedGroupsPlusAuthenticated = append(expectedGroupsPlusAuthenticated, "system:authenticated") // Validate that `pinniped whoami` returns the correct identity. assertWhoami( @@ -337,7 +480,24 @@ status: true, pinnipedExe, kubeconfigPath, - env.SupervisorUpstreamOIDC.Username, + expectedUsername, expectedGroupsPlusAuthenticated, ) } + +func runPinnipedGetKubeconfig(t *testing.T, env *library.TestEnv, pinnipedExe string, tempDir string, pinnipedCLICommand []string) string { + // Run "pinniped get kubeconfig" to get a kubeconfig YAML. + envVarsWithProxy := append(os.Environ(), env.ProxyEnv()...) + kubeconfigYAML, stderr := runPinnipedCLI(t, envVarsWithProxy, pinnipedExe, pinnipedCLICommand...) + t.Logf("stderr output from 'pinniped get kubeconfig':\n%s\n\n", stderr) + t.Logf("test kubeconfig:\n%s\n\n", kubeconfigYAML) + + restConfig := library.NewRestConfigFromKubeconfig(t, kubeconfigYAML) + require.NotNil(t, restConfig.ExecProvider) + require.Equal(t, []string{"login", "oidc"}, restConfig.ExecProvider.Args[:2]) + + kubeconfigPath := filepath.Join(tempDir, "kubeconfig.yaml") + require.NoError(t, ioutil.WriteFile(kubeconfigPath, []byte(kubeconfigYAML), 0600)) + + return kubeconfigPath +} From 41d3e3b6ecbf05831842bb7311347cd5e10458f1 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 11:24:00 -0700 Subject: [PATCH 45/59] Fix lint error in e2e_test.go --- test/integration/e2e_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 59192796..b7f387e5 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -359,7 +359,7 @@ func TestE2EFullIntegration(t *testing.T) { }) } -func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) string { +func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) { readFromFile := "" library.RequireEventuallyWithoutError(t, func() (bool, error) { @@ -373,8 +373,6 @@ func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) strin } return false, nil // keep waiting and reading }, 1*time.Minute, 1*time.Second) - - return readFromFile } func readAvailableOutput(t *testing.T, r io.Reader) (string, bool) { @@ -383,9 +381,8 @@ func readAvailableOutput(t *testing.T, r io.Reader) (string, bool) { if err != nil { if err == io.EOF { return string(buf[:n]), true - } else { - require.NoError(t, err) } + require.NoError(t, err) } return string(buf[:n]), false } From 6c2a775c9bef70bdc9df4eb526bb0e6eb5922db7 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 11:34:16 -0700 Subject: [PATCH 46/59] Use proxy for `pinniped get kubeconfig` in hack/prepare-supervisor-on-kind.sh Because the command now calls the discovery endpoint, so it needs to go through the proxy to resolve the hostname. --- hack/prepare-supervisor-on-kind.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/prepare-supervisor-on-kind.sh b/hack/prepare-supervisor-on-kind.sh index 2d7c7d44..8b6d5969 100755 --- a/hack/prepare-supervisor-on-kind.sh +++ b/hack/prepare-supervisor-on-kind.sh @@ -199,7 +199,7 @@ sleep 5 go build ./cmd/pinniped # Use the CLI to get the kubeconfig. Tell it that you don't want the browser to automatically open for logins. -./pinniped get kubeconfig --oidc-skip-browser >kubeconfig +https_proxy="$PINNIPED_TEST_PROXY" no_proxy="127.0.0.1" ./pinniped get kubeconfig --oidc-skip-browser >kubeconfig # Clear the local CLI cache to ensure that the kubectl command below will need to perform a fresh login. rm -f "$HOME/.config/pinniped/sessions.yaml" From 3008d1a85cc499bfd5d16f2e7611bf67e07a5f0f Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 11:59:48 -0700 Subject: [PATCH 47/59] Log slow LDAP authentication attempts for debugging purposes --- internal/upstreamldap/upstreamldap.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index f163f59d..614a101e 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -12,6 +12,9 @@ import ( "fmt" "net" "strings" + "time" + + "k8s.io/utils/trace" "github.com/go-ldap/ldap/v3" "k8s.io/apiserver/pkg/authentication/authenticator" @@ -226,33 +229,42 @@ func (p *Provider) AuthenticateUser(ctx context.Context, username, password stri } func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticator.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 + err := p.validateConfig() if err != nil { + p.traceAuthFailure(t, err) return nil, false, err } if len(username) == 0 { // Empty passwords are already handled by go-ldap. + p.traceAuthFailure(t, fmt.Errorf("empty username")) return nil, false, nil } conn, err := p.dial(ctx) if err != nil { + p.traceAuthFailure(t, err) return nil, false, 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 { + p.traceAuthFailure(t, err) return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err) } mappedUsername, mappedUID, 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. + p.traceAuthFailure(t, fmt.Errorf("bad username or password")) return nil, false, nil } @@ -263,6 +275,7 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi Groups: []string{}, // Support for group search coming soon. }, } + p.traceAuthSuccess(t) return response, true, nil } @@ -384,3 +397,16 @@ func (p *Provider) getSearchResultAttributeValue(attributeName string, fromUserE return attributeValue, nil } + +func (p *Provider) traceAuthFailure(t *trace.Trace, err error) { + t.Step("authentication failed", + trace.Field{Key: "authenticated", Value: false}, + trace.Field{Key: "reason", Value: err.Error()}, + ) +} + +func (p *Provider) traceAuthSuccess(t *trace.Trace) { + t.Step("authentication succeeded", + trace.Field{Key: "authenticated", Value: true}, + ) +} From 9ca72fcd30165d244c6bf707f3f358c5b741fc26 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 12:57:10 -0700 Subject: [PATCH 48/59] login.go: Respect `overallTimeout` for LDAP login-related http requests Signed-off-by: Matt Moyer --- pkg/oidcclient/login.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index 9908b792..d8539090 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -404,7 +404,7 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) ( } // Send an authorize request. - authCtx, authorizeCtxCancelFunc := context.WithTimeout(context.Background(), httpRequestTimeout) + authCtx, authorizeCtxCancelFunc := context.WithTimeout(h.ctx, httpRequestTimeout) defer authorizeCtxCancelFunc() authReq, err := http.NewRequestWithContext(authCtx, http.MethodGet, authorizeURL, nil) if err != nil { @@ -454,7 +454,7 @@ func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) ( // Exchange the authorization code for access, ID, and refresh tokens and perform required // validations on the returned ID token. - tokenCtx, tokenCtxCancelFunc := context.WithTimeout(context.Background(), httpRequestTimeout) + tokenCtx, tokenCtxCancelFunc := context.WithTimeout(h.ctx, httpRequestTimeout) defer tokenCtxCancelFunc() token, err := h.getProvider(h.oauth2Config, h.provider, h.httpClient). ExchangeAuthcodeAndValidateTokens( From 044443f31500b00ca8d5aa62cf0cacac90734c5a Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 13:06:08 -0700 Subject: [PATCH 49/59] Rename `X-Pinniped-Idp-*` headers to `Pinniped-*` See RFC6648 which asks that people stop using `X-` on header names. Also Matt preferred not mentioning "IDP" in the header name. Signed-off-by: Matt Moyer --- internal/oidc/auth/auth_handler.go | 4 ++-- internal/oidc/auth/auth_handler_test.go | 4 ++-- pkg/oidcclient/login.go | 4 ++-- pkg/oidcclient/login_test.go | 4 ++-- test/integration/supervisor_login_test.go | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 12b0e915..21aad56c 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -27,8 +27,8 @@ import ( ) const ( - CustomUsernameHeaderName = "X-Pinniped-Idp-Username" - CustomPasswordHeaderName = "X-Pinniped-Idp-Password" //nolint:gosec // this is not a credential + CustomUsernameHeaderName = "Pinniped-Username" + CustomPasswordHeaderName = "Pinniped-Password" //nolint:gosec // this is not a credential ) func NewHandler( diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index efb164e3..98da84bc 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -1119,10 +1119,10 @@ func TestAuthorizationEndpoint(t *testing.T) { req.Header.Set("Cookie", test.csrfCookie) } if test.customUsernameHeader != nil { - req.Header.Set("X-Pinniped-Idp-Username", *test.customUsernameHeader) + req.Header.Set("Pinniped-Username", *test.customUsernameHeader) } if test.customPasswordHeader != nil { - req.Header.Set("X-Pinniped-Idp-Password", *test.customPasswordHeader) + req.Header.Set("Pinniped-Password", *test.customPasswordHeader) } rsp := httptest.NewRecorder() subject.ServeHTTP(rsp, req) diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index d8539090..e2d0e2bf 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -52,8 +52,8 @@ const ( supervisorAuthorizeUpstreamNameParam = "pinniped_idp_name" supervisorAuthorizeUpstreamTypeParam = "pinniped_idp_type" - supervisorAuthorizeUpstreamUsernameHeader = "X-Pinniped-Idp-Username" - supervisorAuthorizeUpstreamPasswordHeader = "X-Pinniped-Idp-Password" // nolint:gosec // this is not a credential + supervisorAuthorizeUpstreamUsernameHeader = "Pinniped-Username" + supervisorAuthorizeUpstreamPasswordHeader = "Pinniped-Password" // nolint:gosec // this is not a credential defaultLDAPUsernamePrompt = "Username: " defaultLDAPPasswordPrompt = "Password: " diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index bf5ded20..4cc23f93 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -893,8 +893,8 @@ func TestLogin(t *testing.T) { // nolint:gocyclo return defaultDiscoveryResponse(req) case "http://" + successServer.Listener.Addr().String() + "/authorize": authorizeRequestWasMade = true - require.Equal(t, "some-upstream-username", req.Header.Get("X-Pinniped-Idp-Username")) - require.Equal(t, "some-upstream-password", req.Header.Get("X-Pinniped-Idp-Password")) + require.Equal(t, "some-upstream-username", req.Header.Get("Pinniped-Username")) + require.Equal(t, "some-upstream-password", req.Header.Get("Pinniped-Password")) require.Equal(t, url.Values{ // This is the PKCE challenge which is calculated as base64(sha256("test-pkce")). For example: // $ echo -n test-pkce | shasum -a 256 | cut -d" " -f1 | xxd -r -p | base64 | cut -d"=" -f1 diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 2cba7da0..fbfa8d7b 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -464,8 +464,8 @@ func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAutho require.NoError(t, err) // Set the custom username/password headers for the LDAP authorize request. - authRequest.Header.Set("X-Pinniped-Idp-Username", upstreamUsername) - authRequest.Header.Set("X-Pinniped-Idp-Password", upstreamPassword) + authRequest.Header.Set("Pinniped-Username", upstreamUsername) + authRequest.Header.Set("Pinniped-Password", upstreamPassword) authResponse, err := httpClient.Do(authRequest) require.NoError(t, err) From f0652c1ce161472accae9f3ac8a46d88a65fd94d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 13:20:00 -0700 Subject: [PATCH 50/59] Replace all usages of strPtr() with pointer.StringPtr() --- internal/config/concierge/config.go | 19 ++--- internal/config/concierge/config_test.go | 15 ++-- internal/config/supervisor/config.go | 7 +- internal/config/supervisor/config_test.go | 6 +- internal/oidc/auth/auth_handler_test.go | 69 +++++++++---------- .../registry/credentialrequest/rest_test.go | 7 +- .../concierge_credentialrequest_test.go | 7 +- 7 files changed, 57 insertions(+), 73 deletions(-) diff --git a/internal/config/concierge/config.go b/internal/config/concierge/config.go index 610caa7e..ad6a00df 100644 --- a/internal/config/concierge/config.go +++ b/internal/config/concierge/config.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "strings" + "k8s.io/utils/pointer" "sigs.k8s.io/yaml" "go.pinniped.dev/internal/constable" @@ -69,27 +70,27 @@ func FromPath(path string) (*Config, error) { func maybeSetAPIDefaults(apiConfig *APIConfigSpec) { if apiConfig.ServingCertificateConfig.DurationSeconds == nil { - apiConfig.ServingCertificateConfig.DurationSeconds = int64Ptr(aboutAYear) + apiConfig.ServingCertificateConfig.DurationSeconds = pointer.Int64Ptr(aboutAYear) } if apiConfig.ServingCertificateConfig.RenewBeforeSeconds == nil { - apiConfig.ServingCertificateConfig.RenewBeforeSeconds = int64Ptr(about9Months) + apiConfig.ServingCertificateConfig.RenewBeforeSeconds = pointer.Int64Ptr(about9Months) } } func maybeSetAPIGroupSuffixDefault(apiGroupSuffix **string) { if *apiGroupSuffix == nil { - *apiGroupSuffix = stringPtr(groupsuffix.PinnipedDefaultSuffix) + *apiGroupSuffix = pointer.StringPtr(groupsuffix.PinnipedDefaultSuffix) } } func maybeSetKubeCertAgentDefaults(cfg *KubeCertAgentSpec) { if cfg.NamePrefix == nil { - cfg.NamePrefix = stringPtr("pinniped-kube-cert-agent-") + cfg.NamePrefix = pointer.StringPtr("pinniped-kube-cert-agent-") } if cfg.Image == nil { - cfg.Image = stringPtr("debian:latest") + cfg.Image = pointer.StringPtr("debian:latest") } } @@ -146,11 +147,3 @@ func validateAPI(apiConfig *APIConfigSpec) error { func validateAPIGroupSuffix(apiGroupSuffix string) error { return groupsuffix.Validate(apiGroupSuffix) } - -func int64Ptr(i int64) *int64 { - return &i -} - -func stringPtr(s string) *string { - return &s -} diff --git a/internal/config/concierge/config_test.go b/internal/config/concierge/config_test.go index 1101d2d5..d58ecb2c 100644 --- a/internal/config/concierge/config_test.go +++ b/internal/config/concierge/config_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "k8s.io/utils/pointer" "go.pinniped.dev/internal/here" "go.pinniped.dev/internal/plog" @@ -55,7 +56,7 @@ func TestFromPath(t *testing.T) { `), wantConfig: &Config{ DiscoveryInfo: DiscoveryInfoSpec{ - URL: stringPtr("https://some.discovery/url"), + URL: pointer.StringPtr("https://some.discovery/url"), }, APIConfig: APIConfigSpec{ ServingCertificateConfig: ServingCertificateConfigSpec{ @@ -63,7 +64,7 @@ func TestFromPath(t *testing.T) { RenewBeforeSeconds: int64Ptr(2400), }, }, - APIGroupSuffix: stringPtr("some.suffix.com"), + APIGroupSuffix: pointer.StringPtr("some.suffix.com"), NamesConfig: NamesConfigSpec{ ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuer: "pinniped-config", @@ -80,8 +81,8 @@ func TestFromPath(t *testing.T) { "myLabelKey2": "myLabelValue2", }, KubeCertAgentConfig: KubeCertAgentSpec{ - NamePrefix: stringPtr("kube-cert-agent-name-prefix-"), - Image: stringPtr("kube-cert-agent-image"), + NamePrefix: pointer.StringPtr("kube-cert-agent-name-prefix-"), + Image: pointer.StringPtr("kube-cert-agent-image"), ImagePullSecrets: []string{"kube-cert-agent-image-pull-secret"}, }, LogLevel: plog.LevelDebug, @@ -106,7 +107,7 @@ func TestFromPath(t *testing.T) { DiscoveryInfo: DiscoveryInfoSpec{ URL: nil, }, - APIGroupSuffix: stringPtr("pinniped.dev"), + APIGroupSuffix: pointer.StringPtr("pinniped.dev"), APIConfig: APIConfigSpec{ ServingCertificateConfig: ServingCertificateConfigSpec{ DurationSeconds: int64Ptr(60 * 60 * 24 * 365), // about a year @@ -126,8 +127,8 @@ func TestFromPath(t *testing.T) { }, Labels: map[string]string{}, KubeCertAgentConfig: KubeCertAgentSpec{ - NamePrefix: stringPtr("pinniped-kube-cert-agent-"), - Image: stringPtr("debian:latest"), + NamePrefix: pointer.StringPtr("pinniped-kube-cert-agent-"), + Image: pointer.StringPtr("debian:latest"), }, }, }, diff --git a/internal/config/supervisor/config.go b/internal/config/supervisor/config.go index 24668f54..608a7719 100644 --- a/internal/config/supervisor/config.go +++ b/internal/config/supervisor/config.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "strings" + "k8s.io/utils/pointer" "sigs.k8s.io/yaml" "go.pinniped.dev/internal/constable" @@ -54,7 +55,7 @@ func FromPath(path string) (*Config, error) { func maybeSetAPIGroupSuffixDefault(apiGroupSuffix **string) { if *apiGroupSuffix == nil { - *apiGroupSuffix = stringPtr(groupsuffix.PinnipedDefaultSuffix) + *apiGroupSuffix = pointer.StringPtr(groupsuffix.PinnipedDefaultSuffix) } } @@ -72,7 +73,3 @@ func validateNames(names *NamesConfigSpec) error { } return nil } - -func stringPtr(s string) *string { - return &s -} diff --git a/internal/config/supervisor/config_test.go b/internal/config/supervisor/config_test.go index 7fb1acc8..72839aeb 100644 --- a/internal/config/supervisor/config_test.go +++ b/internal/config/supervisor/config_test.go @@ -8,6 +8,8 @@ import ( "os" "testing" + "k8s.io/utils/pointer" + "github.com/stretchr/testify/require" "go.pinniped.dev/internal/here" @@ -32,7 +34,7 @@ func TestFromPath(t *testing.T) { defaultTLSCertificateSecret: my-secret-name `), wantConfig: &Config{ - APIGroupSuffix: stringPtr("some.suffix.com"), + APIGroupSuffix: pointer.StringPtr("some.suffix.com"), Labels: map[string]string{ "myLabelKey1": "myLabelValue1", "myLabelKey2": "myLabelValue2", @@ -50,7 +52,7 @@ func TestFromPath(t *testing.T) { defaultTLSCertificateSecret: my-secret-name `), wantConfig: &Config{ - APIGroupSuffix: stringPtr("pinniped.dev"), + APIGroupSuffix: pointer.StringPtr("pinniped.dev"), Labels: map[string]string{}, NamesConfig: NamesConfigSpec{ DefaultTLSCertificateSecret: "my-secret-name", diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 98da84bc..d65dbf4b 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -21,6 +21,7 @@ import ( "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/here" "go.pinniped.dev/internal/oidc" @@ -377,8 +378,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: happyGetRequestPath, - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: htmlContentType, wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, @@ -436,8 +437,8 @@ func TestAuthorizationEndpoint(t *testing.T) { path: "/some/path", contentType: "application/x-www-form-urlencoded", body: encodeQuery(happyGetRequestQueryMap), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: htmlContentType, wantRedirectLocationRegexp: happyAuthcodeDownstreamRedirectLocationRegexp, @@ -518,8 +519,8 @@ func TestAuthorizationEndpoint(t *testing.T) { path: modifiedHappyGetRequestPath(map[string]string{ "redirect_uri": downstreamRedirectURIWithDifferentPort, // not the same port number that is registered for the client }), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: htmlContentType, wantRedirectLocationRegexp: downstreamRedirectURIWithDifferentPort + `\?code=([^&]+)&scope=openid&state=` + happyState, @@ -558,8 +559,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&erroringUpstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: happyGetRequestPath, - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusBadGateway, wantContentType: htmlContentType, wantBodyString: "Bad Gateway: unexpected error during upstream authentication\n", @@ -569,8 +570,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: happyGetRequestPath, - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr("wrong-password"), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr("wrong-password"), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), @@ -581,8 +582,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: happyGetRequestPath, - customUsernameHeader: stringPtr("wrong-username"), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr("wrong-username"), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), @@ -594,7 +595,7 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, path: happyGetRequestPath, customUsernameHeader: nil, // do not send header - customPasswordHeader: stringPtr(happyLDAPPassword), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), @@ -605,7 +606,7 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: happyGetRequestPath, - customUsernameHeader: stringPtr(happyLDAPUsername), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), customPasswordHeader: nil, // do not send header wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", @@ -635,8 +636,8 @@ func TestAuthorizationEndpoint(t *testing.T) { path: modifiedHappyGetRequestPath(map[string]string{ "redirect_uri": "http://127.0.0.1/does-not-match-what-is-configured-for-pinniped-cli-client", }), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusBadRequest, wantContentType: "application/json; charset=utf-8", wantBodyJSON: fositeInvalidRedirectURIErrorBody, @@ -709,8 +710,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), @@ -784,8 +785,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge": ""}), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeErrorQuery), @@ -812,8 +813,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "this-is-not-a-valid-pkce-alg"}), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidCodeChallengeErrorQuery), @@ -840,8 +841,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": "plain"}), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), @@ -868,8 +869,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"code_challenge_method": ""}), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingCodeChallengeMethodErrorQuery), @@ -900,8 +901,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login"}), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositePromptHasNoneAndOtherValueErrorQuery), @@ -934,8 +935,8 @@ func TestAuthorizationEndpoint(t *testing.T) { method: http.MethodGet, // The following prompt value is illegal when openid is requested, but note that openid is not requested. path: modifiedHappyGetRequestPath(map[string]string{"prompt": "none login", "scope": "email"}), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: htmlContentType, wantRedirectLocationRegexp: downstreamRedirectURI + `\?code=([^&]+)&scope=&state=` + happyState, // no scopes granted @@ -970,8 +971,8 @@ func TestAuthorizationEndpoint(t *testing.T) { idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), method: http.MethodGet, path: modifiedHappyGetRequestPath(map[string]string{"state": "short"}), - customUsernameHeader: stringPtr(happyLDAPUsername), - customPasswordHeader: stringPtr(happyLDAPPassword), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), wantStatus: http.StatusFound, wantContentType: "application/json; charset=utf-8", wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidStateErrorQuery), @@ -1331,7 +1332,3 @@ func requireEqualURLs(t *testing.T, actualURL string, expectedURL string, ignore } require.Equal(t, expectedLocationQuery, actualLocationQuery) } - -func stringPtr(s string) *string { - return &s -} diff --git a/internal/registry/credentialrequest/rest_test.go b/internal/registry/credentialrequest/rest_test.go index 9c05c2bb..78d5dd73 100644 --- a/internal/registry/credentialrequest/rest_test.go +++ b/internal/registry/credentialrequest/rest_test.go @@ -21,6 +21,7 @@ import ( genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/klog/v2" + "k8s.io/utils/pointer" loginapi "go.pinniped.dev/generated/latest/apis/concierge/login" "go.pinniped.dev/internal/issuer" @@ -347,7 +348,7 @@ func requireSuccessfulResponseWithAuthenticationFailureMessage(t *testing.T, err require.Equal(t, response, &loginapi.TokenCredentialRequest{ Status: loginapi.TokenCredentialRequestStatus{ Credential: nil, - Message: stringPtr("authentication failed"), + Message: pointer.StringPtr("authentication failed"), }, }) } @@ -359,7 +360,3 @@ func successfulIssuer(ctrl *gomock.Controller) issuer.ClientCertIssuer { Return([]byte("test-cert"), []byte("test-key"), nil) return clientCertIssuer } - -func stringPtr(s string) *string { - return &s -} diff --git a/test/integration/concierge_credentialrequest_test.go b/test/integration/concierge_credentialrequest_test.go index 20b86347..8931859d 100644 --- a/test/integration/concierge_credentialrequest_test.go +++ b/test/integration/concierge_credentialrequest_test.go @@ -16,6 +16,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1" @@ -147,7 +148,7 @@ func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthentic require.Empty(t, response.Spec) require.Nil(t, response.Status.Credential) - require.Equal(t, stringPtr("authentication failed"), response.Status.Message) + require.Equal(t, pointer.StringPtr("authentication failed"), response.Status.Message) } func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken(t *testing.T) { @@ -177,10 +178,6 @@ func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken(t *testing.T require.Nil(t, response.Status.Credential) } -func stringPtr(s string) *string { - return &s -} - func getCommonName(t *testing.T, certPEM string) string { t.Helper() From 4804c837d47153486bb3e42038aaf9f9dd6029dc Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 13:37:01 -0700 Subject: [PATCH 51/59] Insignificant change in ldap_upstream_watcher_test.go --- .../upstreamwatcher/ldap_upstream_watcher_test.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go index 89f817a9..9e90cdd1 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go @@ -137,11 +137,7 @@ func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) // Wrap the func into a struct so the test can do deep equal assertions on instances of upstreamldap.Provider. type comparableDialer struct { - f upstreamldap.LDAPDialerFunc -} - -func (d *comparableDialer) Dial(ctx context.Context, hostAndPort string) (upstreamldap.Conn, error) { - return d.f(ctx, hostAndPort) + upstreamldap.LDAPDialerFunc } func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { @@ -689,7 +685,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { tt.setupMocks(conn) } - dialer := &comparableDialer{f: upstreamldap.LDAPDialerFunc(func(ctx context.Context, _ string) (upstreamldap.Conn, error) { + dialer := &comparableDialer{upstreamldap.LDAPDialerFunc(func(ctx context.Context, _ string) (upstreamldap.Conn, error) { if tt.dialError != nil { return nil, tt.dialError } From 22092e9aed9b81f5c62136e15f18c6a71249ccbe Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 14:00:26 -0700 Subject: [PATCH 52/59] Missed a usage of int64Ptr in previous commit --- internal/config/concierge/config_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/config/concierge/config_test.go b/internal/config/concierge/config_test.go index d58ecb2c..baebf8c3 100644 --- a/internal/config/concierge/config_test.go +++ b/internal/config/concierge/config_test.go @@ -60,8 +60,8 @@ func TestFromPath(t *testing.T) { }, APIConfig: APIConfigSpec{ ServingCertificateConfig: ServingCertificateConfigSpec{ - DurationSeconds: int64Ptr(3600), - RenewBeforeSeconds: int64Ptr(2400), + DurationSeconds: pointer.Int64Ptr(3600), + RenewBeforeSeconds: pointer.Int64Ptr(2400), }, }, APIGroupSuffix: pointer.StringPtr("some.suffix.com"), @@ -110,8 +110,8 @@ func TestFromPath(t *testing.T) { APIGroupSuffix: pointer.StringPtr("pinniped.dev"), APIConfig: APIConfigSpec{ ServingCertificateConfig: ServingCertificateConfigSpec{ - DurationSeconds: int64Ptr(60 * 60 * 24 * 365), // about a year - RenewBeforeSeconds: int64Ptr(60 * 60 * 24 * 30 * 9), // about 9 months + DurationSeconds: pointer.Int64Ptr(60 * 60 * 24 * 365), // about a year + RenewBeforeSeconds: pointer.Int64Ptr(60 * 60 * 24 * 30 * 9), // about 9 months }, }, NamesConfig: NamesConfigSpec{ From 1ae3c6a1ad91080d49ec0c45d2960effea63e12d Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 14:00:39 -0700 Subject: [PATCH 53/59] Split package upstreamwatchers into four packages --- cmd/pinniped-supervisor/main.go | 7 +- .../conditionsutil/conditions_util.go.go | 68 ++++++++++++++ .../ldap_upstream_watcher.go | 27 +++--- .../ldap_upstream_watcher_test.go | 6 +- .../oidc_upstream_watcher.go | 91 ++++--------------- .../oidc_upstream_watcher_test.go | 6 +- .../upstreamwatchers/upstream_watchers.go | 16 ++++ 7 files changed, 126 insertions(+), 95 deletions(-) create mode 100644 internal/controller/conditionsutil/conditions_util.go.go rename internal/controller/supervisorconfig/{upstreamwatcher => ldapupstreamwatcher}/ldap_upstream_watcher.go (93%) rename internal/controller/supervisorconfig/{upstreamwatcher => ldapupstreamwatcher}/ldap_upstream_watcher_test.go (99%) rename internal/controller/supervisorconfig/{upstreamwatcher => oidcupstreamwatcher}/oidc_upstream_watcher.go (82%) rename internal/controller/supervisorconfig/{upstreamwatcher => oidcupstreamwatcher}/oidc_upstream_watcher_test.go (99%) create mode 100644 internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 7d196333..0cf897ae 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -32,7 +32,8 @@ import ( "go.pinniped.dev/internal/config/supervisor" "go.pinniped.dev/internal/controller/supervisorconfig" "go.pinniped.dev/internal/controller/supervisorconfig/generator" - "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatcher" + "go.pinniped.dev/internal/controller/supervisorconfig/ldapupstreamwatcher" + "go.pinniped.dev/internal/controller/supervisorconfig/oidcupstreamwatcher" "go.pinniped.dev/internal/controller/supervisorstorage" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/deploymentref" @@ -233,7 +234,7 @@ func startControllers( singletonWorker, ). WithController( - upstreamwatcher.NewOIDCUpstreamWatcherController( + oidcupstreamwatcher.New( dynamicUpstreamIDPProvider, pinnipedClient, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), @@ -243,7 +244,7 @@ func startControllers( ), singletonWorker). WithController( - upstreamwatcher.NewLDAPUpstreamWatcherController( + ldapupstreamwatcher.New( dynamicUpstreamIDPProvider, pinnipedClient, pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), diff --git a/internal/controller/conditionsutil/conditions_util.go.go b/internal/controller/conditionsutil/conditions_util.go.go new file mode 100644 index 00000000..67f13b03 --- /dev/null +++ b/internal/controller/conditionsutil/conditions_util.go.go @@ -0,0 +1,68 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package conditionsutil + +import ( + "sort" + + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/equality" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" +) + +// Merge merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. +func Merge(conditions []*v1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]v1alpha1.Condition, log logr.Logger) bool { + hadErrorCondition := false + for i := range conditions { + cond := conditions[i].DeepCopy() + cond.LastTransitionTime = v1.Now() + cond.ObservedGeneration = observedGeneration + if mergeCondition(conditionsToUpdate, cond) { + log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) + } + if cond.Status == v1alpha1.ConditionFalse { + hadErrorCondition = true + } + } + sort.SliceStable(*conditionsToUpdate, func(i, j int) bool { + return (*conditionsToUpdate)[i].Type < (*conditionsToUpdate)[j].Type + }) + return hadErrorCondition +} + +// mergeCondition merges a new v1alpha1.Condition into a slice of existing conditions. It returns true +// if the condition has meaningfully changed. +func mergeCondition(existing *[]v1alpha1.Condition, new *v1alpha1.Condition) bool { + // Find any existing condition with a matching type. + var old *v1alpha1.Condition + for i := range *existing { + if (*existing)[i].Type == new.Type { + old = &(*existing)[i] + continue + } + } + + // If there is no existing condition of this type, append this one and we're done. + if old == nil { + *existing = append(*existing, *new) + return true + } + + // Set the LastTransitionTime depending on whether the status has changed. + new = new.DeepCopy() + if old.Status == new.Status { + new.LastTransitionTime = old.LastTransitionTime + } + + // If anything has actually changed, update the entry and return true. + if !equality.Semantic.DeepEqual(old, new) { + *old = *new + return true + } + + // Otherwise the entry is already up to date. + return false +} diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go similarity index 93% rename from internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go rename to internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index 274cccd4..6c70c0ec 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -1,7 +1,8 @@ // Copyright 2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package upstreamwatcher +// Package ldapupstreamwatcher implements a controller which watches LDAPIdentityProviders. +package ldapupstreamwatcher import ( "context" @@ -22,6 +23,8 @@ import ( pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1" pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controller/conditionsutil" + "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/upstreamldap" @@ -58,8 +61,8 @@ type ldapWatcherController struct { secretInformer corev1informers.SecretInformer } -// NewLDAPUpstreamWatcherController instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. -func NewLDAPUpstreamWatcherController( +// New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. +func New( idpCache UpstreamLDAPIdentityProviderICache, client pinnipedclientset.Interface, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, @@ -178,7 +181,7 @@ func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentit ca := x509.NewCertPool() ok := ca.AppendCertsFromPEM(bundle) if !ok { - return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", errNoCertificates)) + return c.invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", upstreamwatchers.ErrNoCertificates)) } config.CABundle = bundle @@ -219,7 +222,7 @@ func (c *ldapWatcherController) testConnection( return &v1alpha1.Condition{ Type: typeLDAPConnectionValid, Status: v1alpha1.ConditionTrue, - Reason: reasonSuccess, + Reason: upstreamwatchers.ReasonSuccess, Message: fmt.Sprintf(`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, config.Host, config.BindUsername, upstream.Spec.Bind.SecretName, currentSecretVersion), } @@ -248,7 +251,7 @@ func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Cond return &v1alpha1.Condition{ Type: typeTLSConfigurationValid, Status: v1alpha1.ConditionTrue, - Reason: reasonSuccess, + Reason: upstreamwatchers.ReasonSuccess, Message: message, } } @@ -257,7 +260,7 @@ func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Co return &v1alpha1.Condition{ Type: typeTLSConfigurationValid, Status: v1alpha1.ConditionFalse, - Reason: reasonInvalidTLSConfig, + Reason: upstreamwatchers.ReasonInvalidTLSConfig, Message: message, } } @@ -270,7 +273,7 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr return &v1alpha1.Condition{ Type: typeBindSecretValid, Status: v1alpha1.ConditionFalse, - Reason: reasonNotFound, + Reason: upstreamwatchers.ReasonNotFound, Message: err.Error(), }, "" } @@ -279,7 +282,7 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr return &v1alpha1.Condition{ Type: typeBindSecretValid, Status: v1alpha1.ConditionFalse, - Reason: reasonWrongType, + Reason: upstreamwatchers.ReasonWrongType, Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", secretName, secret.Type, corev1.SecretTypeBasicAuth), }, secret.ResourceVersion @@ -291,7 +294,7 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr return &v1alpha1.Condition{ Type: typeBindSecretValid, Status: v1alpha1.ConditionFalse, - Reason: reasonMissingKeys, + Reason: upstreamwatchers.ReasonMissingKeys, Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", secretName, []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey}), }, secret.ResourceVersion @@ -300,7 +303,7 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr return &v1alpha1.Condition{ Type: typeBindSecretValid, Status: v1alpha1.ConditionTrue, - Reason: reasonSuccess, + Reason: upstreamwatchers.ReasonSuccess, Message: "loaded bind secret", }, secret.ResourceVersion } @@ -309,7 +312,7 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1al log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() - hadErrorCondition := mergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) + hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) updated.Status.Phase = v1alpha1.LDAPPhaseReady if hadErrorCondition { diff --git a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go similarity index 99% rename from internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go rename to internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index 9e90cdd1..1875e7e2 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -1,7 +1,7 @@ // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package upstreamwatcher +package ldapupstreamwatcher import ( "context" @@ -78,7 +78,7 @@ func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { secretInformer := kubeInformers.Core().V1().Secrets() withInformer := testutil.NewObservableWithInformerOption() - NewLDAPUpstreamWatcherController(nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) + New(nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) unrelated := corev1.Secret{} filter := withInformer.GetFilterForInformer(secretInformer) @@ -123,7 +123,7 @@ func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) secretInformer := kubeInformers.Core().V1().Secrets() withInformer := testutil.NewObservableWithInformerOption() - NewLDAPUpstreamWatcherController(nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) + New(nil, nil, ldapIDPInformer, secretInformer, withInformer.WithInformer) unrelated := corev1.Secret{} filter := withInformer.GetFilterForInformer(ldapIDPInformer) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go similarity index 82% rename from internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go rename to internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 609e1679..d1b3cb78 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -1,8 +1,8 @@ // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package upstreamwatcher implements controllers that watch the idp.supervisor.pinniped.dev API group's objects. -package upstreamwatcher +// Package oidcupstreamwatcher implements a controller which watches OIDCIdentityProviders. +package oidcupstreamwatcher import ( "context" @@ -31,6 +31,8 @@ import ( idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1" "go.pinniped.dev/internal/constable" pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controller/conditionsutil" + "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/upstreamoidc" @@ -52,17 +54,12 @@ const ( // Constants related to conditions. typeClientCredentialsValid = "ClientCredentialsValid" typeOIDCDiscoverySucceeded = "OIDCDiscoverySucceeded" - reasonNotFound = "SecretNotFound" - reasonWrongType = "SecretWrongType" - reasonMissingKeys = "SecretMissingKeys" - reasonSuccess = "Success" - reasonUnreachable = "Unreachable" - reasonInvalidTLSConfig = "InvalidTLSConfig" - reasonInvalidResponse = "InvalidResponse" + + reasonUnreachable = "Unreachable" + reasonInvalidResponse = "InvalidResponse" // Errors that are generated by our reconcile process. errOIDCFailureStatus = constable.Error("OIDCIdentityProvider has a failing condition") - errNoCertificates = constable.Error("no certificates found") ) // UpstreamOIDCIdentityProviderICache is a thread safe cache that holds a list of validated upstream OIDC IDP configurations. @@ -111,8 +108,8 @@ type oidcWatcherController struct { } } -// NewOIDCUpstreamWatcherController instantiates a new controllerlib.Controller which will populate the provided UpstreamOIDCIdentityProviderICache. -func NewOIDCUpstreamWatcherController( +// New instantiates a new controllerlib.Controller which will populate the provided UpstreamOIDCIdentityProviderICache. +func New( idpCache UpstreamOIDCIdentityProviderICache, client pinnipedclientset.Interface, oidcIdentityProviderInformer idpinformers.OIDCIdentityProviderInformer, @@ -212,7 +209,7 @@ func (c *oidcWatcherController) validateSecret(upstream *v1alpha1.OIDCIdentityPr return &v1alpha1.Condition{ Type: typeClientCredentialsValid, Status: v1alpha1.ConditionFalse, - Reason: reasonNotFound, + Reason: upstreamwatchers.ReasonNotFound, Message: err.Error(), } } @@ -222,7 +219,7 @@ func (c *oidcWatcherController) validateSecret(upstream *v1alpha1.OIDCIdentityPr return &v1alpha1.Condition{ Type: typeClientCredentialsValid, Status: v1alpha1.ConditionFalse, - Reason: reasonWrongType, + Reason: upstreamwatchers.ReasonWrongType, Message: fmt.Sprintf("referenced Secret %q has wrong type %q (should be %q)", secretName, secret.Type, oidcClientSecretType), } } @@ -234,7 +231,7 @@ func (c *oidcWatcherController) validateSecret(upstream *v1alpha1.OIDCIdentityPr return &v1alpha1.Condition{ Type: typeClientCredentialsValid, Status: v1alpha1.ConditionFalse, - Reason: reasonMissingKeys, + Reason: upstreamwatchers.ReasonMissingKeys, Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", secretName, []string{clientIDDataKey, clientSecretDataKey}), } } @@ -245,7 +242,7 @@ func (c *oidcWatcherController) validateSecret(upstream *v1alpha1.OIDCIdentityPr return &v1alpha1.Condition{ Type: typeClientCredentialsValid, Status: v1alpha1.ConditionTrue, - Reason: reasonSuccess, + Reason: upstreamwatchers.ReasonSuccess, Message: "loaded client credentials", } } @@ -262,7 +259,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1 return &v1alpha1.Condition{ Type: typeOIDCDiscoverySucceeded, Status: v1alpha1.ConditionFalse, - Reason: reasonInvalidTLSConfig, + Reason: upstreamwatchers.ReasonInvalidTLSConfig, Message: err.Error(), } } @@ -314,7 +311,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1 return &v1alpha1.Condition{ Type: typeOIDCDiscoverySucceeded, Status: v1alpha1.ConditionTrue, - Reason: reasonSuccess, + Reason: upstreamwatchers.ReasonSuccess, Message: "discovered issuer configuration", } } @@ -335,7 +332,7 @@ func (*oidcWatcherController) getTLSConfig(upstream *v1alpha1.OIDCIdentityProvid result.RootCAs = x509.NewCertPool() if !result.RootCAs.AppendCertsFromPEM(bundle) { - return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", errNoCertificates) + return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", upstreamwatchers.ErrNoCertificates) } return &result, nil @@ -345,7 +342,7 @@ func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1al log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() - hadErrorCondition := mergeConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) + hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) updated.Status.Phase = v1alpha1.PhaseReady if hadErrorCondition { @@ -365,60 +362,6 @@ func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1al } } -// mergeConditions merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. -func mergeConditions(conditions []*v1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]v1alpha1.Condition, log logr.Logger) bool { - hadErrorCondition := false - for i := range conditions { - cond := conditions[i].DeepCopy() - cond.LastTransitionTime = metav1.Now() - cond.ObservedGeneration = observedGeneration - if mergeCondition(conditionsToUpdate, cond) { - log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) - } - if cond.Status == v1alpha1.ConditionFalse { - hadErrorCondition = true - } - } - sort.SliceStable(*conditionsToUpdate, func(i, j int) bool { - return (*conditionsToUpdate)[i].Type < (*conditionsToUpdate)[j].Type - }) - return hadErrorCondition -} - -// mergeCondition merges a new v1alpha1.Condition into a slice of existing conditions. It returns true -// if the condition has meaningfully changed. -func mergeCondition(existing *[]v1alpha1.Condition, new *v1alpha1.Condition) bool { - // Find any existing condition with a matching type. - var old *v1alpha1.Condition - for i := range *existing { - if (*existing)[i].Type == new.Type { - old = &(*existing)[i] - continue - } - } - - // If there is no existing condition of this type, append this one and we're done. - if old == nil { - *existing = append(*existing, *new) - return true - } - - // Set the LastTransitionTime depending on whether the status has changed. - new = new.DeepCopy() - if old.Status == new.Status { - new.LastTransitionTime = old.LastTransitionTime - } - - // If anything has actually changed, update the entry and return true. - if !equality.Semantic.DeepEqual(old, new) { - *old = *new - return true - } - - // Otherwise the entry is already up to date. - return false -} - func (*oidcWatcherController) computeScopes(additionalScopes []string) []string { // First compute the unique set of scopes, including "openid" (de-duplicate). set := make(map[string]bool, len(additionalScopes)+1) diff --git a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go similarity index 99% rename from internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go rename to internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go index 06b82c02..9370aad3 100644 --- a/internal/controller/supervisorconfig/upstreamwatcher/oidc_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher_test.go @@ -1,7 +1,7 @@ // Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package upstreamwatcher +package oidcupstreamwatcher import ( "context" @@ -82,7 +82,7 @@ func TestOIDCUpstreamWatcherControllerFilterSecret(t *testing.T) { secretInformer := kubeInformers.Core().V1().Secrets() withInformer := testutil.NewObservableWithInformerOption() - NewOIDCUpstreamWatcherController( + New( cache, nil, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), @@ -762,7 +762,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + testIs &upstreamoidc.ProviderConfig{Name: "initial-entry"}, }) - controller := NewOIDCUpstreamWatcherController( + controller := New( cache, fakePinnipedClient, pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(), diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go new file mode 100644 index 00000000..36bd37c8 --- /dev/null +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -0,0 +1,16 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package upstreamwatchers + +import "go.pinniped.dev/internal/constable" + +const ( + ReasonNotFound = "SecretNotFound" + ReasonWrongType = "SecretWrongType" + ReasonMissingKeys = "SecretMissingKeys" + ReasonSuccess = "Success" + ReasonInvalidTLSConfig = "InvalidTLSConfig" + + ErrNoCertificates = constable.Error("no certificates found") +) From 29ca8acab4eec30d3d2f94bbd9cdc4a0df3898ef Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 12 May 2021 14:05:08 -0700 Subject: [PATCH 54/59] oidc_upstream_watcher.go: two methods become private funcs --- .../oidc_upstream_watcher.go | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index d1b3cb78..b610a2c6 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -170,7 +170,7 @@ func (c *oidcWatcherController) validateUpstream(ctx controllerlib.Context, upst result := upstreamoidc.ProviderConfig{ Name: upstream.Name, Config: &oauth2.Config{ - Scopes: c.computeScopes(upstream.Spec.AuthorizationConfig.AdditionalScopes), + Scopes: computeScopes(upstream.Spec.AuthorizationConfig.AdditionalScopes), }, UsernameClaim: upstream.Spec.Claims.Username, GroupsClaim: upstream.Spec.Claims.Groups, @@ -254,7 +254,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1 // If the provider does not exist in the cache, do a fresh discovery lookup and save to the cache. if discoveredProvider == nil { - tlsConfig, err := c.getTLSConfig(upstream) + tlsConfig, err := getTLSConfig(upstream) if err != nil { return &v1alpha1.Condition{ Type: typeOIDCDiscoverySucceeded, @@ -316,28 +316,6 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1 } } -func (*oidcWatcherController) getTLSConfig(upstream *v1alpha1.OIDCIdentityProvider) (*tls.Config, error) { - result := tls.Config{ - MinVersion: tls.VersionTLS12, - } - - if upstream.Spec.TLS == nil || upstream.Spec.TLS.CertificateAuthorityData == "" { - return &result, nil - } - - bundle, err := base64.StdEncoding.DecodeString(upstream.Spec.TLS.CertificateAuthorityData) - if err != nil { - return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", err) - } - - result.RootCAs = x509.NewCertPool() - if !result.RootCAs.AppendCertsFromPEM(bundle) { - return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", upstreamwatchers.ErrNoCertificates) - } - - return &result, nil -} - func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.OIDCIdentityProvider, conditions []*v1alpha1.Condition) { log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() @@ -362,7 +340,29 @@ func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1al } } -func (*oidcWatcherController) computeScopes(additionalScopes []string) []string { +func getTLSConfig(upstream *v1alpha1.OIDCIdentityProvider) (*tls.Config, error) { + result := tls.Config{ + MinVersion: tls.VersionTLS12, + } + + if upstream.Spec.TLS == nil || upstream.Spec.TLS.CertificateAuthorityData == "" { + return &result, nil + } + + bundle, err := base64.StdEncoding.DecodeString(upstream.Spec.TLS.CertificateAuthorityData) + if err != nil { + return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", err) + } + + result.RootCAs = x509.NewCertPool() + if !result.RootCAs.AppendCertsFromPEM(bundle) { + return nil, fmt.Errorf("spec.certificateAuthorityData is invalid: %w", upstreamwatchers.ErrNoCertificates) + } + + return &result, nil +} + +func computeScopes(additionalScopes []string) []string { // First compute the unique set of scopes, including "openid" (de-duplicate). set := make(map[string]bool, len(additionalScopes)+1) set["openid"] = true From 67dca688d748c62397e0c366a0b757179aa3cb1e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 13 May 2021 10:05:56 -0700 Subject: [PATCH 55/59] Add an API version to the Supervisor IDP discovery endpoint Also rename one of the new functional opts in login.go to more accurately reflect the intention of the opt. --- cmd/pinniped/cmd/kubeconfig.go | 14 +- cmd/pinniped/cmd/kubeconfig_test.go | 153 ++++++++++++------ cmd/pinniped/cmd/login_oidc.go | 2 +- internal/oidc/discovery/discovery_handler.go | 8 +- .../oidc/discovery/discovery_handler_test.go | 12 +- internal/oidc/oidc.go | 2 +- internal/oidc/provider/manager/manager.go | 2 +- .../oidc/provider/manager/manager_test.go | 4 +- pkg/oidcclient/login.go | 19 +-- pkg/oidcclient/login_test.go | 4 +- 10 files changed, 147 insertions(+), 73 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 832b1365..88f2b86d 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -95,11 +95,15 @@ type getKubeconfigParams struct { credentialCachePathSet bool } -type supervisorOIDCDiscoveryResponse struct { +type supervisorOIDCDiscoveryResponseWithV1Alpha1 struct { + SupervisorDiscovery SupervisorDiscoveryResponseV1Alpha1 `json:"discovery.supervisor.pinniped.dev/v1alpha1"` +} + +type SupervisorDiscoveryResponseV1Alpha1 struct { PinnipedIDPsEndpoint string `json:"pinniped_identity_providers_endpoint"` } -type supervisorIDPsDiscoveryResponse struct { +type supervisorIDPsDiscoveryResponseV1Alpha1 struct { PinnipedIDPs []pinnipedIDPResponse `json:"pinniped_identity_providers"` } @@ -800,13 +804,13 @@ func discoverIDPsDiscoveryEndpointURL(ctx context.Context, issuer string, httpCl return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not read response body: %w", err) } - var body supervisorOIDCDiscoveryResponse + var body supervisorOIDCDiscoveryResponseWithV1Alpha1 err = json.Unmarshal(rawBody, &body) if err != nil { return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not parse response JSON: %w", err) } - return body.PinnipedIDPsEndpoint, nil + return body.SupervisorDiscovery.PinnipedIDPsEndpoint, nil } func discoverAllAvailableSupervisorUpstreamIDPs(ctx context.Context, pinnipedIDPsEndpoint string, httpClient *http.Client) ([]pinnipedIDPResponse, error) { @@ -831,7 +835,7 @@ func discoverAllAvailableSupervisorUpstreamIDPs(ctx context.Context, pinnipedIDP return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: could not read response body: %w", err) } - var body supervisorIDPsDiscoveryResponse + var body supervisorIDPsDiscoveryResponseV1Alpha1 err = json.Unmarshal(rawBody, &body) if err != nil { return nil, fmt.Errorf("unable to fetch IDP discovery data from issuer: could not parse response JSON: %w", err) diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index c2223350..8fd00f66 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -74,6 +74,16 @@ func TestGetKubeconfig(t *testing.T) { } } + happyOIDCDIscoveryResponse := func(issuerURL string) string { + return here.Docf(`{ + "other-key": "other-value", + "discovery.supervisor.pinniped.dev/v1alpha1": { + "pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers" + }, + "another-key": "another-value" + }`, issuerURL) + } + tests := []struct { name string args func(string, string) []string @@ -737,9 +747,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryStatusCode: http.StatusBadRequest, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -772,9 +780,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, @@ -848,9 +854,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: "this is not valid JSON", wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -958,7 +962,11 @@ func TestGetKubeconfig(t *testing.T) { } }, oidcDiscoveryResponse: func(issuerURL string) string { - return `{"pinniped_identity_providers_endpoint": "https%://illegal_url"}` + return here.Doc(`{ + "discovery.supervisor.pinniped.dev/v1alpha1": { + "pinniped_identity_providers_endpoint": "https%://illegal_url" + } + }`) }, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -990,9 +998,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, @@ -1021,9 +1027,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "my-idp", } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "my-idp", "type": "ldap"}, @@ -1051,9 +1055,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-oidc-idp", "type": "oidc"}, @@ -1079,9 +1081,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "my-nonexistent-idp", } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-oidc-idp", "type": "oidc"}, @@ -1598,9 +1598,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"} @@ -1677,9 +1675,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-oidc-idp", "type": "oidc"} @@ -1756,9 +1752,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [] }`), @@ -1818,7 +1812,7 @@ func TestGetKubeconfig(t *testing.T) { }, }, { - name: "IDP discovery endpoint is not listed in OIDC discovery document", + name: "Supervisor discovery section is not listed in OIDC discovery document", args: func(issuerCABundle string, issuerURL string) []string { return []string{ "--kubeconfig", "./testdata/kubeconfig.yaml", @@ -1890,6 +1884,83 @@ func TestGetKubeconfig(t *testing.T) { base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) }, }, + { + name: "IDP discovery endpoint is not listed in OIDC discovery document within the Supervisor discovery section", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + oidcDiscoveryResponse: func(issuerURL string) string { + return here.Doc(`{ + "discovery.supervisor.pinniped.dev/v1alpha1": { + "wrong-key": "some-value" + } + }`) + }, + idpsDiscoveryStatusCode: http.StatusBadRequest, // IDPs endpoint shouldn't be called by this test + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantStdout: func(issuerCABundle string, issuerURL string) string { + return here.Docf(` + 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 + - oidc + - --enable-concierge + - --concierge-api-group-suffix=pinniped.dev + - --concierge-authenticator-name=test-authenticator + - --concierge-authenticator-type=jwt + - --concierge-endpoint=https://fake-server-url-value + - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --issuer=%s + - --client-id=pinniped-cli + - --scopes=offline_access,openid,pinniped:request-audience + - --ca-bundle-data=%s + - --request-audience=test-audience + command: '.../path/to/pinniped' + env: [] + provideClusterInfo: true + `, + issuerURL, + base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) + }, + }, { name: "when OIDC discovery document 404s, dont set idp related flags", args: func(issuerCABundle string, issuerURL string) []string { @@ -2050,9 +2121,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-other-ldap-idp", "type": "ldap"} @@ -2127,9 +2196,7 @@ func TestGetKubeconfig(t *testing.T) { "--oidc-ca-bundle", f.Name(), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"} @@ -2186,9 +2253,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, @@ -2247,9 +2312,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "some-ldap-idp", } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return fmt.Sprintf(`{"pinniped_identity_providers_endpoint": "%s/pinniped_identity_providers"}`, issuerURL) - }, + oidcDiscoveryResponse: happyOIDCDIscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, @@ -2313,7 +2376,7 @@ func TestGetKubeconfig(t *testing.T) { w.WriteHeader(tt.oidcDiscoveryStatusCode) _, err = w.Write([]byte(jsonResponseBody)) require.NoError(t, err) - case "/pinniped_identity_providers": + case "/v1alpha1/pinniped_identity_providers": jsonResponseBody := tt.idpsDiscoveryResponse if tt.idpsDiscoveryResponse == "" { jsonResponseBody = "{}" diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 8674573f..83542c01 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -160,7 +160,7 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin case "oidc": // this is the default, so don't need to do anything case "ldap": - opts = append(opts, oidcclient.WithLDAPUpstreamIdentityProvider()) + opts = append(opts, oidcclient.WithCLISendingCredentials()) default: // Surprisingly cobra does not support this kind of flag validation. See https://github.com/spf13/pflag/issues/236 return fmt.Errorf( diff --git a/internal/oidc/discovery/discovery_handler.go b/internal/oidc/discovery/discovery_handler.go index 542ef729..e472c012 100644 --- a/internal/oidc/discovery/discovery_handler.go +++ b/internal/oidc/discovery/discovery_handler.go @@ -40,11 +40,15 @@ type Metadata struct { // vvv Custom vvv - PinnipedIDPsEndpoint string `json:"pinniped_identity_providers_endpoint"` + SupervisorDiscovery SupervisorDiscoveryMetadataV1Alpha1 `json:"discovery.supervisor.pinniped.dev/v1alpha1"` // ^^^ Custom ^^^ } +type SupervisorDiscoveryMetadataV1Alpha1 struct { + PinnipedIDPsEndpoint string `json:"pinniped_identity_providers_endpoint"` +} + type IdentityProviderMetadata struct { Name string `json:"name"` Type string `json:"type"` @@ -57,7 +61,7 @@ func NewHandler(issuerURL string) http.Handler { AuthorizationEndpoint: issuerURL + oidc.AuthorizationEndpointPath, TokenEndpoint: issuerURL + oidc.TokenEndpointPath, JWKSURI: issuerURL + oidc.JWKSEndpointPath, - PinnipedIDPsEndpoint: issuerURL + oidc.PinnipedIDPsPath, + SupervisorDiscovery: SupervisorDiscoveryMetadataV1Alpha1{PinnipedIDPsEndpoint: issuerURL + oidc.PinnipedIDPsPathV1Alpha1}, ResponseTypesSupported: []string{"code"}, SubjectTypesSupported: []string{"public"}, IDTokenSigningAlgValuesSupported: []string{"ES256"}, diff --git a/internal/oidc/discovery/discovery_handler_test.go b/internal/oidc/discovery/discovery_handler_test.go index fd37341b..b3c70b35 100644 --- a/internal/oidc/discovery/discovery_handler_test.go +++ b/internal/oidc/discovery/discovery_handler_test.go @@ -35,11 +35,13 @@ func TestDiscovery(t *testing.T) { wantStatus: http.StatusOK, wantContentType: "application/json", wantBodyJSON: &Metadata{ - Issuer: "https://some-issuer.com/some/path", - AuthorizationEndpoint: "https://some-issuer.com/some/path/oauth2/authorize", - TokenEndpoint: "https://some-issuer.com/some/path/oauth2/token", - JWKSURI: "https://some-issuer.com/some/path/jwks.json", - PinnipedIDPsEndpoint: "https://some-issuer.com/some/path/pinniped_identity_providers", + Issuer: "https://some-issuer.com/some/path", + AuthorizationEndpoint: "https://some-issuer.com/some/path/oauth2/authorize", + TokenEndpoint: "https://some-issuer.com/some/path/oauth2/token", + JWKSURI: "https://some-issuer.com/some/path/jwks.json", + SupervisorDiscovery: SupervisorDiscoveryMetadataV1Alpha1{ + PinnipedIDPsEndpoint: "https://some-issuer.com/some/path/v1alpha1/pinniped_identity_providers", + }, ResponseTypesSupported: []string{"code"}, SubjectTypesSupported: []string{"public"}, IDTokenSigningAlgValuesSupported: []string{"ES256"}, diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index 3be60dcb..78319329 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -24,7 +24,7 @@ const ( TokenEndpointPath = "/oauth2/token" //nolint:gosec // ignore lint warning that this is a credential CallbackEndpointPath = "/callback" JWKSEndpointPath = "/jwks.json" - PinnipedIDPsPath = "/pinniped_identity_providers" + PinnipedIDPsPathV1Alpha1 = "/v1alpha1/pinniped_identity_providers" ) const ( diff --git a/internal/oidc/provider/manager/manager.go b/internal/oidc/provider/manager/manager.go index 1d41e4ef..ea1d2d62 100644 --- a/internal/oidc/provider/manager/manager.go +++ b/internal/oidc/provider/manager/manager.go @@ -107,7 +107,7 @@ func (m *Manager) SetProviders(federationDomains ...*provider.FederationDomainIs m.providerHandlers[(issuerHostWithPath + oidc.JWKSEndpointPath)] = jwks.NewHandler(issuer, m.dynamicJWKSProvider) - m.providerHandlers[(issuerHostWithPath + oidc.PinnipedIDPsPath)] = idpdiscovery.NewHandler(m.upstreamIDPs) + m.providerHandlers[(issuerHostWithPath + oidc.PinnipedIDPsPathV1Alpha1)] = idpdiscovery.NewHandler(m.upstreamIDPs) m.providerHandlers[(issuerHostWithPath + oidc.AuthorizationEndpointPath)] = auth.NewHandler( issuer, diff --git a/internal/oidc/provider/manager/manager_test.go b/internal/oidc/provider/manager/manager_test.go index c99fadff..469a085d 100644 --- a/internal/oidc/provider/manager/manager_test.go +++ b/internal/oidc/provider/manager/manager_test.go @@ -86,13 +86,13 @@ func TestManager(t *testing.T) { err = json.Unmarshal(responseBody, &parsedDiscoveryResult) r.NoError(err) r.Equal(expectedIssuer, parsedDiscoveryResult.Issuer) - r.Equal(parsedDiscoveryResult.PinnipedIDPsEndpoint, expectedIssuer+oidc.PinnipedIDPsPath) + r.Equal(parsedDiscoveryResult.SupervisorDiscovery.PinnipedIDPsEndpoint, expectedIssuer+oidc.PinnipedIDPsPathV1Alpha1) } requirePinnipedIDPsDiscoveryRequestToBeHandled := func(requestIssuer, requestURLSuffix, expectedIDPName, expectedIDPType string) { recorder := httptest.NewRecorder() - subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.PinnipedIDPsPath+requestURLSuffix)) + subject.ServeHTTP(recorder, newGetRequest(requestIssuer+oidc.PinnipedIDPsPathV1Alpha1+requestURLSuffix)) r.False(fallbackHandlerWasCalled) diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index e2d0e2bf..40739eb8 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -74,7 +74,7 @@ type handlerState struct { upstreamIdentityProviderName string upstreamIdentityProviderType string - ldapUpstreamIdentityProvider bool + cliToSendCredentials bool requestedAudience string @@ -200,14 +200,15 @@ func WithRequestAudience(audience string) Option { } } -// WithLDAPUpstreamIdentityProvider causes the login flow to use CLI prompts for username and password and causes the +// WithCLISendingCredentials causes the login flow to use CLI-based prompts for username and password and causes the // call to the Issuer's authorize endpoint to be made directly (no web browser) with the username and password on custom // HTTP headers. This is only intended to be used when the issuer is a Pinniped Supervisor and the upstream identity -// provider is an LDAP provider. It should never be used with non-Supervisor issuers because it will send the user's -// password as a custom header, which would be ignored but could potentially get logged somewhere by the issuer. -func WithLDAPUpstreamIdentityProvider() Option { +// provider type supports this style of authentication. Currently this is supported by LDAPIdentityProviders. +// This should never be used with non-Supervisor issuers because it will send the user's password to the authorization +// endpoint as a custom header, which would be ignored but could potentially get logged somewhere by the issuer. +func WithCLISendingCredentials() Option { return func(h *handlerState) error { - h.ldapUpstreamIdentityProvider = true + h.cliToSendCredentials = true return nil } } @@ -356,7 +357,7 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { // Choose the appropriate authorization and authcode exchange strategy. var authFunc = h.webBrowserBasedAuth - if h.ldapUpstreamIdentityProvider { + if h.cliToSendCredentials { authFunc = h.cliBasedAuth } @@ -371,8 +372,8 @@ func (h *handlerState) baseLogin() (*oidctypes.Token, error) { return token, err } -// Make a direct call to the authorize endpoint and parse the authcode from the response. -// Exchange the authcode for tokens. Return the tokens or an error. +// Make a direct call to the authorize endpoint, including the user's username and password on custom http headers, +// and parse the authcode from the response. Exchange the authcode for tokens. Return the tokens or an error. func (h *handlerState) cliBasedAuth(authorizeOptions *[]oauth2.AuthCodeOption) (*oidctypes.Token, error) { // Ask the user for their username and password. username, err := h.promptForValue(defaultLDAPUsernamePrompt) diff --git a/pkg/oidcclient/login_test.go b/pkg/oidcclient/login_test.go index 4cc23f93..d85c01ac 100644 --- a/pkg/oidcclient/login_test.go +++ b/pkg/oidcclient/login_test.go @@ -232,7 +232,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo require.Equal(t, []SessionCacheKey{cacheKey}, cache.sawGetKeys) }) require.NoError(t, WithSessionCache(cache)(h)) - require.NoError(t, WithLDAPUpstreamIdentityProvider()(h)) + require.NoError(t, WithCLISendingCredentials()(h)) require.NoError(t, WithUpstreamIdentityProvider("some-upstream-name", "ldap")(h)) require.NoError(t, WithClient(&http.Client{ @@ -875,7 +875,7 @@ func TestLogin(t *testing.T) { // nolint:gocyclo require.Equal(t, []*oidctypes.Token{&testToken}, cache.sawPutTokens) }) require.NoError(t, WithSessionCache(cache)(h)) - require.NoError(t, WithLDAPUpstreamIdentityProvider()(h)) + require.NoError(t, WithCLISendingCredentials()(h)) require.NoError(t, WithUpstreamIdentityProvider("some-upstream-name", "ldap")(h)) discoveryRequestWasMade := false From f15fc66e0660d3d102a97d934c64eab269a127ab Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 13 May 2021 12:27:42 -0700 Subject: [PATCH 56/59] `pinniped get kubeconfig` refactor to use oidc.NewProvider for discovery - Note that this adds an extra check of the response, which is that the issuer string in the response must match issuer of the requested URL. - Some of the error messages also changed to match the errors provided by oidc.NewProvider --- cmd/pinniped/cmd/kubeconfig.go | 64 ++++------ cmd/pinniped/cmd/kubeconfig_test.go | 178 +++++++++++++--------------- 2 files changed, 106 insertions(+), 136 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 88f2b86d..32d724a2 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -28,6 +28,7 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" // Adds handlers for various dynamic auth plugins in client-go "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/transport" conciergev1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" @@ -735,18 +736,9 @@ func hasPendingStrategy(credentialIssuer *configv1alpha1.CredentialIssuer) bool } func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigParams) error { - transport := &http.Transport{ - TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, - Proxy: http.ProxyFromEnvironment, - } - httpClient := &http.Client{Transport: transport} - if flags.oidc.caBundle != nil { - rootCAs := x509.NewCertPool() - ok := rootCAs.AppendCertsFromPEM(flags.oidc.caBundle) - if !ok { - return fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not parse CA bundle") - } - transport.TLSClientConfig.RootCAs = rootCAs + httpClient, err := newDiscoveryHTTPClient(flags.oidc.caBundle) + if err != nil { + return err } pinnipedIDPsEndpoint, err := discoverIDPsDiscoveryEndpointURL(ctx, flags.oidc.issuer, httpClient) @@ -776,38 +768,34 @@ func discoverSupervisorUpstreamIDP(ctx context.Context, flags *getKubeconfigPara return nil } +func newDiscoveryHTTPClient(caBundleFlag caBundleFlag) (*http.Client, error) { + t := &http.Transport{ + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + Proxy: http.ProxyFromEnvironment, + } + httpClient := &http.Client{Transport: t} + if caBundleFlag != nil { + rootCAs := x509.NewCertPool() + ok := rootCAs.AppendCertsFromPEM(caBundleFlag) + if !ok { + return nil, fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not parse CA bundle") + } + t.TLSClientConfig.RootCAs = rootCAs + } + httpClient.Transport = transport.DebugWrappers(httpClient.Transport) + return httpClient, nil +} + func discoverIDPsDiscoveryEndpointURL(ctx context.Context, issuer string, httpClient *http.Client) (string, error) { - issuerDiscoveryURL := issuer + "/.well-known/openid-configuration" - request, err := http.NewRequestWithContext(ctx, http.MethodGet, issuerDiscoveryURL, nil) + discoveredProvider, err := oidc.NewProvider(oidc.ClientContext(ctx, httpClient), issuer) if err != nil { - return "", fmt.Errorf("while forming request to issuer URL: %w", err) - } - - response, err := httpClient.Do(request) - if err != nil { - return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: %w", err) - } - defer func() { - _ = response.Body.Close() - }() - if response.StatusCode == http.StatusNotFound { - // 404 Not Found is not an error because OIDC discovery is an optional part of the OIDC spec. - return "", nil - } - if response.StatusCode != http.StatusOK { - // Other types of error responses aside from 404 are not expected. - return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: unexpected http response status: %s", response.Status) - } - - rawBody, err := ioutil.ReadAll(response.Body) - if err != nil { - return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not read response body: %w", err) + return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err) } var body supervisorOIDCDiscoveryResponseWithV1Alpha1 - err = json.Unmarshal(rawBody, &body) + err = discoveredProvider.Claims(&body) if err != nil { - return "", fmt.Errorf("unable to fetch OIDC discovery data from issuer: could not parse response JSON: %w", err) + return "", fmt.Errorf("while fetching OIDC discovery data from issuer: %w", err) } return body.SupervisorDiscovery.PinnipedIDPsEndpoint, nil diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index 8fd00f66..2853b0e4 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -74,13 +74,21 @@ func TestGetKubeconfig(t *testing.T) { } } - happyOIDCDIscoveryResponse := func(issuerURL string) string { + happyOIDCDiscoveryResponse := func(issuerURL string) string { return here.Docf(`{ + "issuer": "%s", "other-key": "other-value", "discovery.supervisor.pinniped.dev/v1alpha1": { "pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers" }, "another-key": "another-value" + }`, issuerURL, issuerURL) + } + + onlyIssuerOIDCDiscoveryResponse := func(issuerURL string) string { + return here.Docf(`{ + "issuer": "%s", + "other-key": "other-value" }`, issuerURL) } @@ -643,6 +651,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, @@ -730,7 +739,45 @@ func TestGetKubeconfig(t *testing.T) { }, wantError: true, wantStderr: func(issuerCABundle string, issuerURL string) string { - return "Error: unable to fetch OIDC discovery data from issuer: unexpected http response status: 400 Bad Request\n" + return "Error: while fetching OIDC discovery data from issuer: 400 Bad Request: {}\n" + }, + }, + { + name: "when OIDC discovery document lists the wrong issuer", + args: func(issuerCABundle string, issuerURL string) []string { + return []string{ + "--kubeconfig", "./testdata/kubeconfig.yaml", + "--skip-validation", + } + }, + conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { + return []runtime.Object{ + credentialIssuer(), + jwtAuthenticator(issuerCABundle, issuerURL), + } + }, + oidcDiscoveryResponse: func(issuerURL string) string { + return here.Doc(`{ + "issuer": "https://wrong-issuer.com" + }`) + }, + 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 JWTAuthenticator" "name"="test-authenticator"`, + fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), + `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, + `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, + } + }, + wantError: true, + wantStderr: func(issuerCABundle string, issuerURL string) string { + return fmt.Sprintf( + "Error: while fetching OIDC discovery data from issuer: oidc: issuer did not match the issuer returned by provider, expected \"%s\" got \"https://wrong-issuer.com\"\n", + issuerURL) }, }, { @@ -747,7 +794,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryStatusCode: http.StatusBadRequest, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -780,7 +827,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, @@ -837,7 +884,7 @@ func TestGetKubeconfig(t *testing.T) { }, wantError: true, wantStderr: func(issuerCABundle string, issuerURL string) string { - return "Error: unable to fetch OIDC discovery data from issuer: could not parse response JSON: invalid character 'h' in literal true (expecting 'r')\n" + return "Error: while fetching OIDC discovery data from issuer: oidc: failed to decode provider discovery object: got Content-Type = application/json, but could not unmarshal as JSON: invalid character 'h' in literal true (expecting 'r')\n" }, }, { @@ -854,7 +901,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: "this is not valid JSON", wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -906,7 +953,7 @@ func TestGetKubeconfig(t *testing.T) { }, wantError: true, wantStderr: func(issuerCABundle string, issuerURL string) string { - return fmt.Sprintf("Error: unable to fetch OIDC discovery data from issuer: Get \"%s/.well-known/openid-configuration\": x509: certificate signed by unknown authority\n", issuerURL) + return fmt.Sprintf("Error: while fetching OIDC discovery data from issuer: Get \"%s/.well-known/openid-configuration\": x509: certificate signed by unknown authority\n", issuerURL) }, }, { @@ -944,7 +991,7 @@ func TestGetKubeconfig(t *testing.T) { }, wantError: true, wantStderr: func(issuerCABundle string, issuerURL string) string { - return `Error: while forming request to issuer URL: parse "https%://bad-issuer-url/.well-known/openid-configuration": first path segment in URL cannot contain colon` + "\n" + return `Error: while fetching OIDC discovery data from issuer: parse "https%://bad-issuer-url/.well-known/openid-configuration": first path segment in URL cannot contain colon` + "\n" }, }, { @@ -962,11 +1009,12 @@ func TestGetKubeconfig(t *testing.T) { } }, oidcDiscoveryResponse: func(issuerURL string) string { - return here.Doc(`{ + return here.Docf(`{ + "issuer": "%s", "discovery.supervisor.pinniped.dev/v1alpha1": { - "pinniped_identity_providers_endpoint": "https%://illegal_url" + "pinniped_identity_providers_endpoint": "https%%://illegal_url" } - }`) + }`, issuerURL) }, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -998,7 +1046,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, @@ -1027,7 +1075,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "my-idp", } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "my-idp", "type": "ldap"}, @@ -1055,7 +1103,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-oidc-idp", "type": "oidc"}, @@ -1081,7 +1129,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "my-nonexistent-idp", } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-oidc-idp", "type": "oidc"}, @@ -1232,6 +1280,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, @@ -1319,7 +1368,8 @@ func TestGetKubeconfig(t *testing.T) { &conciergev1alpha1.WebhookAuthenticator{ObjectMeta: metav1.ObjectMeta{Name: "test-authenticator"}}, } }, - wantLogs: func(issuerCABundle string, issuerURL string) []string { return nil }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, + wantLogs: func(issuerCABundle string, issuerURL string) []string { return nil }, wantStdout: func(issuerCABundle string, issuerURL string) string { return here.Docf(` apiVersion: v1 @@ -1424,6 +1474,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, @@ -1529,6 +1580,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ `"level"=0 "msg"="discovered CredentialIssuer" "name"="test-credential-issuer"`, @@ -1598,7 +1650,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"} @@ -1675,7 +1727,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-oidc-idp", "type": "oidc"} @@ -1752,7 +1804,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [] }`), @@ -1825,9 +1877,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: func(issuerURL string) string { - return `{"other_field": "other_value"}` - }, + oidcDiscoveryResponse: onlyIssuerOIDCDiscoveryResponse, idpsDiscoveryStatusCode: http.StatusBadRequest, // IDPs endpoint shouldn't be called by this test wantLogs: func(issuerCABundle string, issuerURL string) []string { return []string{ @@ -1899,11 +1949,12 @@ func TestGetKubeconfig(t *testing.T) { } }, oidcDiscoveryResponse: func(issuerURL string) string { - return here.Doc(`{ + return here.Docf(`{ + "issuer": "%s", "discovery.supervisor.pinniped.dev/v1alpha1": { "wrong-key": "some-value" } - }`) + }`, issuerURL) }, idpsDiscoveryStatusCode: http.StatusBadRequest, // IDPs endpoint shouldn't be called by this test wantLogs: func(issuerCABundle string, issuerURL string) []string { @@ -1961,76 +2012,6 @@ func TestGetKubeconfig(t *testing.T) { base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) }, }, - { - name: "when OIDC discovery document 404s, dont set idp related flags", - args: func(issuerCABundle string, issuerURL string) []string { - return []string{ - "--kubeconfig", "./testdata/kubeconfig.yaml", - "--skip-validation", - } - }, - conciergeObjects: func(issuerCABundle string, issuerURL string) []runtime.Object { - return []runtime.Object{ - credentialIssuer(), - jwtAuthenticator(issuerCABundle, issuerURL), - } - }, - oidcDiscoveryStatusCode: http.StatusNotFound, - 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 JWTAuthenticator" "name"="test-authenticator"`, - fmt.Sprintf(`"level"=0 "msg"="discovered OIDC issuer" "issuer"="%s"`, issuerURL), - `"level"=0 "msg"="discovered OIDC audience" "audience"="test-audience"`, - `"level"=0 "msg"="discovered OIDC CA bundle" "roots"=1`, - } - }, - wantStdout: func(issuerCABundle string, issuerURL string) string { - return here.Docf(` - 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 - - oidc - - --enable-concierge - - --concierge-api-group-suffix=pinniped.dev - - --concierge-authenticator-name=test-authenticator - - --concierge-authenticator-type=jwt - - --concierge-endpoint=https://fake-server-url-value - - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== - - --issuer=%s - - --client-id=pinniped-cli - - --scopes=offline_access,openid,pinniped:request-audience - - --ca-bundle-data=%s - - --request-audience=test-audience - command: '.../path/to/pinniped' - env: [] - provideClusterInfo: true - `, - issuerURL, - base64.StdEncoding.EncodeToString([]byte(issuerCABundle))) - }, - }, { name: "when upstream idp related flags are sent, pass them through", args: func(issuerCABundle string, issuerURL string) []string { @@ -2121,7 +2102,7 @@ func TestGetKubeconfig(t *testing.T) { jwtAuthenticator(issuerCABundle, issuerURL), } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-other-ldap-idp", "type": "ldap"} @@ -2196,7 +2177,7 @@ func TestGetKubeconfig(t *testing.T) { "--oidc-ca-bundle", f.Name(), } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"} @@ -2253,7 +2234,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-type", "ldap", } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, @@ -2312,7 +2293,7 @@ func TestGetKubeconfig(t *testing.T) { "--upstream-identity-provider-name", "some-ldap-idp", } }, - oidcDiscoveryResponse: happyOIDCDIscoveryResponse, + oidcDiscoveryResponse: happyOIDCDiscoveryResponse, idpsDiscoveryResponse: here.Docf(`{ "pinniped_identity_providers": [ {"name": "some-ldap-idp", "type": "ldap"}, @@ -2364,6 +2345,7 @@ func TestGetKubeconfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var issuerEndpointPtr *string issuerCABundle, issuerEndpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") switch r.URL.Path { case "/.well-known/openid-configuration": jsonResponseBody := "{}" From 609883c49ea7e19d1b54897687d3efbcf970c721 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 13 May 2021 13:07:31 -0700 Subject: [PATCH 57/59] Update TestSupervisorOIDCDiscovery for versioned IDP discovery endpoint --- test/integration/supervisor_discovery_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration/supervisor_discovery_test.go b/test/integration/supervisor_discovery_test.go index 07b58de0..6740d644 100644 --- a/test/integration/supervisor_discovery_test.go +++ b/test/integration/supervisor_discovery_test.go @@ -482,11 +482,11 @@ func requireWellKnownEndpointIsWorking(t *testing.T, supervisorScheme, superviso "scopes_supported": ["openid", "offline"], "response_types_supported": ["code"], "claims_supported": ["groups"], + "discovery.supervisor.pinniped.dev/v1alpha1": {"pinniped_identity_providers_endpoint": "%s/v1alpha1/pinniped_identity_providers"}, "subject_types_supported": ["public"], - "id_token_signing_alg_values_supported": ["ES256"], - "pinniped_idps": [] + "id_token_signing_alg_values_supported": ["ES256"] }`) - expectedJSON := fmt.Sprintf(expectedResultTemplate, issuerName, issuerName, issuerName, issuerName) + expectedJSON := fmt.Sprintf(expectedResultTemplate, issuerName, issuerName, issuerName, issuerName, issuerName) require.Equal(t, "application/json", response.Header.Get("content-type")) require.JSONEq(t, expectedJSON, responseBody) From f5bf8978a364860c691d1d5b2d43491aef059ec1 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 13 May 2021 15:22:36 -0700 Subject: [PATCH 58/59] Cache ResourceVersion of the validated bind Secret in memory ...instead of caching it in the text of the Condition message --- .../ldap_upstream_watcher.go | 103 ++++++++++++------ .../ldap_upstream_watcher_test.go | 54 ++++++--- 2 files changed, 110 insertions(+), 47 deletions(-) diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index 6c70c0ec..5484e424 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -9,7 +9,6 @@ import ( "crypto/x509" "encoding/base64" "fmt" - "regexp" "time" corev1 "k8s.io/api/core/v1" @@ -44,10 +43,6 @@ const ( loadedTLSConfigurationMessage = "loaded TLS configuration" ) -var ( - secretVersionParser = regexp.MustCompile(` \[validated with Secret ".+" at version "(.+)"]`) -) - // UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. type UpstreamLDAPIdentityProviderICache interface { SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) @@ -55,12 +50,23 @@ type UpstreamLDAPIdentityProviderICache interface { type ldapWatcherController struct { cache UpstreamLDAPIdentityProviderICache + validatedSecretVersionsCache *secretVersionCache ldapDialer upstreamldap.LDAPDialer client pinnipedclientset.Interface ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer secretInformer corev1informers.SecretInformer } +// An in-memory cache with an entry for each LDAPIdentityProvider, to keep track of which ResourceVersion +// of the bind Secret was used during the most recent successful validation. +type secretVersionCache struct { + ResourceVersionsByName map[string]string +} + +func newSecretVersionCache() *secretVersionCache { + return &secretVersionCache{ResourceVersionsByName: map[string]string{}} +} + // New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. func New( idpCache UpstreamLDAPIdentityProviderICache, @@ -69,12 +75,23 @@ func New( secretInformer corev1informers.SecretInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { - // nil means to use a real production dialer when creating objects to add to the dynamicUpstreamIDPProvider cache. - return newInternal(idpCache, nil, client, ldapIdentityProviderInformer, secretInformer, withInformer) + return newInternal( + idpCache, + // start with an empty secretVersionCache + newSecretVersionCache(), + // nil means to use a real production dialer when creating objects to add to the cache + nil, + client, + ldapIdentityProviderInformer, + secretInformer, + withInformer, + ) } +// For test dependency injection purposes. func newInternal( idpCache UpstreamLDAPIdentityProviderICache, + validatedSecretVersionsCache *secretVersionCache, ldapDialer upstreamldap.LDAPDialer, client pinnipedclientset.Interface, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, @@ -83,6 +100,7 @@ func newInternal( ) controllerlib.Controller { c := ldapWatcherController{ cache: idpCache, + validatedSecretVersionsCache: validatedSecretVersionsCache, ldapDialer: ldapDialer, client: client, ldapIdentityProviderInformer: ldapIdentityProviderInformer, @@ -113,21 +131,24 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error { requeue := false validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams)) for _, upstream := range actualUpstreams { - valid := c.validateUpstream(ctx.Context, upstream) - if valid == nil { - requeue = true - } else { + valid, requestedRequeue := c.validateUpstream(ctx.Context, upstream) + if valid != nil { validatedUpstreams = append(validatedUpstreams, valid) } + if requestedRequeue { + requeue = true + } } + c.cache.SetLDAPIdentityProviders(validatedUpstreams) + if requeue { return controllerlib.ErrSyntheticRequeue } return nil } -func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI { +func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) { spec := upstream.Spec config := &upstreamldap.ProviderConfig{ @@ -148,20 +169,33 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * conditions = append(conditions, secretValidCondition, tlsValidCondition) // No point in trying to connect to the server if the config was already determined to be invalid. + var finishedConfigCondition *v1alpha1.Condition if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { - finishedConfigCondition := c.validateFinishedConfig(ctx, upstream, config, currentSecretVersion) - // nil when there is no need to update this condition. + finishedConfigCondition = c.validateFinishedConfig(ctx, upstream, config, currentSecretVersion) if finishedConfigCondition != nil { conditions = append(conditions, finishedConfigCondition) } } - hadErrorCondition := c.updateStatus(ctx, upstream, conditions) - if hadErrorCondition { - return nil + c.updateStatus(ctx, upstream, conditions) + + switch { + case secretValidCondition.Status != v1alpha1.ConditionTrue || tlsValidCondition.Status != v1alpha1.ConditionTrue: + // Invalid provider, so do not load it into the cache. + p = nil + requeue = true + case finishedConfigCondition != nil && finishedConfigCondition.Status != v1alpha1.ConditionTrue: + // Error but load it into the cache anyway, treating this condition failure more like a warning. + p = upstreamldap.New(*config) + // Try again hoping that the condition will improve. + requeue = true + default: + // Fully validated provider, so load it into the cache. + p = upstreamldap.New(*config) + requeue = false } - return upstreamldap.New(*config) + return p, requeue } func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { @@ -191,14 +225,23 @@ func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentit func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { ldapProvider := upstreamldap.New(*config) - if hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion) { + if c.hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion) { return nil } testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout) defer cancelFunc() - return c.testConnection(testConnectionTimeout, upstream, config, ldapProvider, currentSecretVersion) + condition := c.testConnection(testConnectionTimeout, upstream, config, ldapProvider, currentSecretVersion) + + if condition.Status == v1alpha1.ConditionTrue { + // Remember (in-memory for this pod) that the controller has successfully validated the LDAP provider + // using this version of the Secret. This is for performance reasons, to avoid attempting to connect to + // the LDAP server more than is needed. If the pod restarts, it will attempt this validation again. + c.validatedSecretVersionsCache.ResourceVersionsByName[upstream.GetName()] = currentSecretVersion + } + + return condition } func (c *ldapWatcherController) testConnection( @@ -228,17 +271,13 @@ func (c *ldapWatcherController) testConnection( } } -func hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool { +func (c *ldapWatcherController) hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool { currentGeneration := upstream.Generation - for _, c := range upstream.Status.Conditions { - if c.Type == typeLDAPConnectionValid && c.Status == v1alpha1.ConditionTrue && c.ObservedGeneration == currentGeneration { + for _, cond := range upstream.Status.Conditions { + if cond.Type == typeLDAPConnectionValid && cond.Status == v1alpha1.ConditionTrue && cond.ObservedGeneration == currentGeneration { // Found a previously successful condition for the current spec generation. - // Now figure out which version of the bind Secret was used during that previous validation. - matches := secretVersionParser.FindStringSubmatch(c.Message) - if len(matches) != 2 { - continue - } - validatedSecretVersion := matches[1] + // Now figure out which version of the bind Secret was used during that previous validation, if any. + validatedSecretVersion := c.validatedSecretVersionsCache.ResourceVersionsByName[upstream.GetName()] if validatedSecretVersion == currentSecretVersion { return true } @@ -308,7 +347,7 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr }, secret.ResourceVersion } -func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) bool { +func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) { log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() @@ -320,7 +359,7 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1al } if equality.Semantic.DeepEqual(upstream, updated) { - return hadErrorCondition + return // nothing to update } _, err := c.client. @@ -330,6 +369,4 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1al if err != nil { log.Error(err, "failed to update status") } - - return hadErrorCondition } diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index 1875e7e2..9b0137d2 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -249,14 +249,16 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { } tests := []struct { - name string - inputUpstreams []runtime.Object - inputSecrets []runtime.Object - setupMocks func(conn *mockldapconn.MockConn) - dialError error - wantErr string - wantResultingCache []*upstreamldap.ProviderConfig - wantResultingUpstreams []v1alpha1.LDAPIdentityProvider + name string + initialValidatedSecretVersions map[string]string + inputUpstreams []runtime.Object + inputSecrets []runtime.Object + setupMocks func(conn *mockldapconn.MockConn) + dialError error + wantErr string + wantResultingCache []*upstreamldap.ProviderConfig + wantResultingUpstreams []v1alpha1.LDAPIdentityProvider + wantValidatedSecretVersions map[string]string }{ { name: "no LDAPIdentityProvider upstreams clears the cache", @@ -279,6 +281,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, + wantValidatedSecretVersions: map[string]string{testName: "4242"}, }, { name: "missing secret", @@ -455,6 +458,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, + wantValidatedSecretVersions: map[string]string{testName: "4242"}, }, { name: "non-nil TLS configuration with empty CertificateAuthorityData is valid", @@ -489,6 +493,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, + wantValidatedSecretVersions: map[string]string{testName: "4242"}, }, { name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", @@ -531,9 +536,10 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }, + wantValidatedSecretVersions: map[string]string{testName: "4242"}, }, { - name: "when testing the connection to the LDAP server fails then the upstream is not added to the cache", + name: "when testing the connection to the LDAP server fails then the upstream is still added to the cache anyway (treated like a warning)", inputUpstreams: []runtime.Object{validUpstream}, inputSecrets: []runtime.Object{validBindUserSecret("")}, setupMocks: func(conn *mockldapconn.MockConn) { @@ -542,7 +548,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { conn.EXPECT().Close().Times(1) }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), - wantResultingCache: []*upstreamldap.ProviderConfig{}, + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream}, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, Status: v1alpha1.LDAPIdentityProviderStatus{ @@ -572,7 +578,8 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ldapConnectionValidTrueCondition(1234, "4242"), } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSecretVersions: map[string]string{testName: "4242"}, setupMocks: func(conn *mockldapconn.MockConn) { // Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called. }, @@ -584,6 +591,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, + wantValidatedSecretVersions: map[string]string{testName: "4242"}, }, { name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again", @@ -593,7 +601,8 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ldapConnectionValidTrueCondition(1233, "4242"), // older spec generation! } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSecretVersions: map[string]string{testName: "4242"}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -607,6 +616,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, + wantValidatedSecretVersions: map[string]string{testName: "4242"}, }, { name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again", @@ -623,7 +633,8 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSecretVersions: map[string]string{testName: "1"}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -637,6 +648,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, + wantValidatedSecretVersions: map[string]string{testName: "4242"}, }, { name: "when the LDAP server connection was already validated for this resource generation but the bind secret has changed, then try to validate it again", @@ -646,7 +658,8 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ldapConnectionValidTrueCondition(1234, "4241"), // same spec generation, old secret version } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! + initialValidatedSecretVersions: map[string]string{testName: "4241"}, // old version was validated setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -660,6 +673,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, + wantValidatedSecretVersions: map[string]string{testName: "4242"}, }, } @@ -692,8 +706,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { return conn, nil })} + validatedSecretVersionCache := newSecretVersionCache() + if tt.initialValidatedSecretVersions != nil { + validatedSecretVersionCache.ResourceVersionsByName = tt.initialValidatedSecretVersions + } + controller := newInternal( cache, + validatedSecretVersionCache, dialer, fakePinnipedClient, pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), @@ -737,6 +757,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { // Require each separately to get a nice diff when the test fails. require.Equal(t, tt.wantResultingUpstreams[i], normalizedActualUpstreams[i]) } + + // Check that the controller remembered which version of the secret it most recently validated successfully with. + if tt.wantValidatedSecretVersions == nil { + tt.wantValidatedSecretVersions = map[string]string{} + } + require.Equal(t, tt.wantValidatedSecretVersions, validatedSecretVersionCache.ResourceVersionsByName) }) } } From 20b1c41bf52985568026f51a1ef62a3bca442992 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 13 May 2021 16:02:24 -0700 Subject: [PATCH 59/59] Experiment to see if we can ignore `read /dev/ptmx: input/output error` This error seems to always happen on linux, but never on MacOS. --- test/integration/e2e_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 4794e39b..d2fc5e46 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -348,8 +348,9 @@ func TestE2EFullIntegration(t *testing.T) { require.NoError(t, err) // Read all of the remaining output from the subprocess until EOF. - remainingOutput, err := ioutil.ReadAll(ptyFile) - require.NoError(t, err) + remainingOutput, _ := ioutil.ReadAll(ptyFile) + // Ignore any errors returned because there is always an error on linux. + require.Greaterf(t, len(remainingOutput), 0, "expected to get some more output from the kubectl subcommand, but did not") require.Greaterf(t, len(strings.Split(string(remainingOutput), "\n")), 2, "expected some namespaces to be returned, got %q", string(remainingOutput)) t.Logf("first kubectl command took %s", time.Since(start).String())