From b06de69f6acf6a1effa95c3303c18607bf861824 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 1 Jul 2021 08:48:52 -0700 Subject: [PATCH 01/36] ActiveDirectoryIdentityProvider - Create CRD - Create implementation of AD-specific user search defaults --- apis/supervisor/idp/v1alpha1/register.go.tmpl | 4 +- ...es_activedirectoryidentityprovider.go.tmpl | 167 ++ .../idp/v1alpha1/types_meta.go.tmpl | 2 +- ....dev_activedirectoryidentityproviders.yaml | 271 ++++ generated/1.17/README.adoc | 153 ++ .../apis/supervisor/idp/v1alpha1/register.go | 4 +- .../types_activedirectoryidentityprovider.go | 167 ++ .../supervisor/idp/v1alpha1/types_meta.go | 2 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 190 +++ .../activedirectoryidentityprovider.go | 178 +++ .../fake_activedirectoryidentityprovider.go | 127 ++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../activedirectoryidentityprovider.go | 76 + .../idp/v1alpha1/interface.go | 7 + .../activedirectoryidentityprovider.go | 81 + .../idp/v1alpha1/expansion_generated.go | 8 + ....dev_activedirectoryidentityproviders.yaml | 271 ++++ generated/1.18/README.adoc | 153 ++ .../apis/supervisor/idp/v1alpha1/register.go | 4 +- .../types_activedirectoryidentityprovider.go | 167 ++ .../supervisor/idp/v1alpha1/types_meta.go | 2 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 190 +++ .../activedirectoryidentityprovider.go | 182 +++ .../fake_activedirectoryidentityprovider.go | 129 ++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../activedirectoryidentityprovider.go | 77 + .../idp/v1alpha1/interface.go | 7 + .../activedirectoryidentityprovider.go | 81 + .../idp/v1alpha1/expansion_generated.go | 8 + ....dev_activedirectoryidentityproviders.yaml | 271 ++++ generated/1.19/README.adoc | 153 ++ .../apis/supervisor/idp/v1alpha1/register.go | 4 +- .../types_activedirectoryidentityprovider.go | 167 ++ .../supervisor/idp/v1alpha1/types_meta.go | 2 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 190 +++ .../activedirectoryidentityprovider.go | 182 +++ .../fake_activedirectoryidentityprovider.go | 129 ++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../activedirectoryidentityprovider.go | 77 + .../idp/v1alpha1/interface.go | 7 + .../activedirectoryidentityprovider.go | 86 ++ .../idp/v1alpha1/expansion_generated.go | 8 + ....dev_activedirectoryidentityproviders.yaml | 271 ++++ generated/1.20/README.adoc | 153 ++ .../apis/supervisor/idp/v1alpha1/register.go | 4 +- .../types_activedirectoryidentityprovider.go | 167 ++ .../supervisor/idp/v1alpha1/types_meta.go | 2 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 190 +++ .../activedirectoryidentityprovider.go | 182 +++ .../fake_activedirectoryidentityprovider.go | 129 ++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../activedirectoryidentityprovider.go | 77 + .../idp/v1alpha1/interface.go | 7 + .../activedirectoryidentityprovider.go | 86 ++ .../idp/v1alpha1/expansion_generated.go | 8 + ....dev_activedirectoryidentityproviders.yaml | 271 ++++ .../apis/supervisor/idp/v1alpha1/register.go | 4 +- .../types_activedirectoryidentityprovider.go | 167 ++ .../supervisor/idp/v1alpha1/types_meta.go | 2 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 190 +++ .../activedirectoryidentityprovider.go | 182 +++ .../fake_activedirectoryidentityprovider.go | 129 ++ .../idp/v1alpha1/fake/fake_idp_client.go | 4 + .../typed/idp/v1alpha1/generated_expansion.go | 2 + .../typed/idp/v1alpha1/idp_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../activedirectoryidentityprovider.go | 77 + .../idp/v1alpha1/interface.go | 7 + .../activedirectoryidentityprovider.go | 86 ++ .../idp/v1alpha1/expansion_generated.go | 8 + go.mod | 1 + go.sum | 1 + internal/upstreamad/upstreamad.go | 654 ++++++++ internal/upstreamad/upstreamad_test.go | 1375 +++++++++++++++++ 86 files changed, 8467 insertions(+), 12 deletions(-) create mode 100644 apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl create mode 100644 deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml create mode 100644 generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go create mode 100644 generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go create mode 100644 generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.17/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml create mode 100644 generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go create mode 100644 generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go create mode 100644 generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.18/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml create mode 100644 generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go create mode 100644 generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go create mode 100644 generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.19/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml create mode 100644 generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go create mode 100644 generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go create mode 100644 generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.20/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml create mode 100644 generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go create mode 100644 generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go create mode 100644 generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 generated/latest/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go create mode 100644 internal/upstreamad/upstreamad.go create mode 100644 internal/upstreamad/upstreamad_test.go diff --git a/apis/supervisor/idp/v1alpha1/register.go.tmpl b/apis/supervisor/idp/v1alpha1/register.go.tmpl index ddc9c3607..e3406a814 100644 --- a/apis/supervisor/idp/v1alpha1/register.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/register.go.tmpl @@ -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 v1alpha1 @@ -34,6 +34,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &OIDCIdentityProviderList{}, &LDAPIdentityProvider{}, &LDAPIdentityProviderList{}, + &ActiveDirectoryIdentityProvider{}, + &ActiveDirectoryIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl new file mode 100644 index 000000000..18726e7bd --- /dev/null +++ b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl @@ -0,0 +1,167 @@ +// 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 ActiveDirectoryIdentityProviderPhase string + +const ( + // ActiveDirectoryPhasePending is the default phase for newly-created ActiveDirectoryIdentityProvider resources. + ActiveDirectoryPhasePending ActiveDirectoryIdentityProviderPhase = "Pending" + + // ActiveDirectoryPhaseReady is the phase for an ActiveDirectoryIdentityProvider resource in a healthy state. + ActiveDirectoryPhaseReady ActiveDirectoryIdentityProviderPhase = "Ready" + + // ActiveDirectoryPhaseError is the phase for an ActiveDirectoryIdentityProvider in an unhealthy state. + ActiveDirectoryPhaseError ActiveDirectoryIdentityProviderPhase = "Error" +) + +// Status of an Active Directory identity provider. +type ActiveDirectoryIdentityProviderStatus struct { + // Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase ActiveDirectoryIdentityProviderPhase `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 ActiveDirectoryIdentityProviderBind struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an Active Directory 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 (distinguished name) 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 ActiveDirectoryIdentityProviderUserSearchAttributes struct { + // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // Optional, when empty this defaults to "sAMAccountName". + // +optional + Username string `json:"username,omitempty"` + + // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely + // identify the user within this ActiveDirectory provider after a successful authentication. + // Optional, when empty this defaults to "objectGUID". + // +optional + UID string `json:"uid,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name + // in the user's list of groups after a successful authentication. + // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + +type ActiveDirectoryIdentityProviderUserSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for users. + // E.g. "ou=users,dc=example,dc=com". + // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is + // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter at least once 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 ActiveDirectory 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. + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as + // the result of the user search. + // +optional + Attributes ActiveDirectoryIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. + // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and + // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. + // The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the + // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or + // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as + // the result of the group search. + // +optional + Attributes ActiveDirectoryIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + +// Spec for configuring an ActiveDirectory identity provider. +type ActiveDirectoryIdentityProviderSpec struct { + // Host is the hostname of this Active Directory 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 *TLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind ActiveDirectoryIdentityProviderBind `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in Active Directory. + UserSearch ActiveDirectoryIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. + GroupSearch ActiveDirectoryIdentityProviderGroupSearch `json:"groupSearch,omitempty"` +} + +// ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory 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 ActiveDirectoryIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec ActiveDirectoryIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status ActiveDirectoryIdentityProviderStatus `json:"status,omitempty"` +} + +// List of ActiveDirectoryIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ActiveDirectoryIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ActiveDirectoryIdentityProvider `json:"items"` +} diff --git a/apis/supervisor/idp/v1alpha1/types_meta.go.tmpl b/apis/supervisor/idp/v1alpha1/types_meta.go.tmpl index e59976ff3..e04c6f2ae 100644 --- a/apis/supervisor/idp/v1alpha1/types_meta.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_meta.go.tmpl @@ -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 v1alpha1 diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml new file mode 100644 index 000000000..03fbcd085 --- /dev/null +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -0,0 +1,271 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: ActiveDirectoryIdentityProvider + listKind: ActiveDirectoryIdentityProviderList + plural: activedirectoryidentityproviders + singular: activedirectoryidentityprovider + 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: ActiveDirectoryIdentityProvider describes the configuration of + an upstream Microsoft Active Directory 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: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the ActiveDirectory 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 + Active Directory 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 (distinguished name) 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: + - secretName + type: object + groupSearch: + description: GroupSearch contains the configuration for searching + for a user's group membership in ActiveDirectory. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each ActiveDirectory entry which was found + as the result of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the Active Directory entries whose value shall become + a group name in the user's list of groups after a successful + authentication. The value of this field is case-sensitive + and must match the case of the attribute name returned by + the ActiveDirectory server in the user's entry. E.g. "cn" + for common name. Distinguished names can be used by specifying + lower-case "dn". Optional. When not specified, the default + will act as if the GroupName were specified as "dn" (distinguished + name). + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". + When not specified, no group search will be performed and authenticated + users will not belong to any groups from the ActiveDirectory + provider. Also, when not specified, the values of Filter and + Attributes are ignored. + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for groups for a user. The + pattern "{}" must occur in the filter at least once and will + be dynamically replaced by the dn (distinguished name) of the + user entry found as a result of the user search. E.g. "member={}" + or "&(objectClass=groupOfNames)(member={})". For more information + about ActiveDirectory 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 "member={}". + type: string + type: object + host: + description: 'Host is the hostname of this Active Directory 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). + 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 Active Directory. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the ActiveDirectory entry which was found as the + result of the user search. + properties: + uid: + description: UID specifies the name of the attribute in the + ActiveDirectory entry which whose value shall be used to + uniquely identify the user within this ActiveDirectory provider + after a successful authentication. Optional, when empty + this defaults to "objectGUID". + type: string + username: + description: Username specifies the name of the attribute + in the ActiveDirectory entry whose value shall become the + username of the user after a successful authentication. + This would typically be the same attribute name used in + Optional, when empty this defaults to "sAMAccountName". + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + Optional, when not specified it will be defaulted based on the + host, for example if your active directory host is "activedirectory.example.com:636", + it will be "dc=activedirectory,dc=example,dc=com". + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for users. The pattern "{}" + must occur in the filter at least once 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 ActiveDirectory 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. + type: string + type: object + required: + - host + type: object + 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 ActiveDirectoryIdentityProvider. + 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.17/README.adoc b/generated/1.17/README.adoc index cb311448b..d0ba80df3 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -748,6 +748,157 @@ 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-activedirectoryidentityprovider"] +==== ActiveDirectoryIdentityProvider + +ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderlist[$$ActiveDirectoryIdentityProviderList$$] +**** + +[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-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderbind"] +==== ActiveDirectoryIdentityProviderBind + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[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 Active Directory 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 (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch"] +==== ActiveDirectoryIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes"] +==== ActiveDirectoryIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec"] +==== ActiveDirectoryIdentityProviderSpec + +Spec for configuring an ActiveDirectory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this Active Directory 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-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-activedirectoryidentityproviderbind[$$ActiveDirectoryIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory 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-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in Active Directory. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus"] +==== ActiveDirectoryIdentityProviderStatus + +Status of an Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __ActiveDirectoryIdentityProviderPhase__ | Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch"] +==== ActiveDirectoryIdentityProviderUserSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be defaulted based on the host, for example if your active directory host is "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes"] +==== ActiveDirectoryIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-condition"] ==== Condition @@ -755,6 +906,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity pro .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$] - 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$$] **** @@ -1054,6 +1206,7 @@ Status of an OIDC identity provider. .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] - 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/register.go b/generated/1.17/apis/supervisor/idp/v1alpha1/register.go index ddc9c3607..e3406a814 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/register.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 v1alpha1 @@ -34,6 +34,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &OIDCIdentityProviderList{}, &LDAPIdentityProvider{}, &LDAPIdentityProviderList{}, + &ActiveDirectoryIdentityProvider{}, + &ActiveDirectoryIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go new file mode 100644 index 000000000..18726e7bd --- /dev/null +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -0,0 +1,167 @@ +// 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 ActiveDirectoryIdentityProviderPhase string + +const ( + // ActiveDirectoryPhasePending is the default phase for newly-created ActiveDirectoryIdentityProvider resources. + ActiveDirectoryPhasePending ActiveDirectoryIdentityProviderPhase = "Pending" + + // ActiveDirectoryPhaseReady is the phase for an ActiveDirectoryIdentityProvider resource in a healthy state. + ActiveDirectoryPhaseReady ActiveDirectoryIdentityProviderPhase = "Ready" + + // ActiveDirectoryPhaseError is the phase for an ActiveDirectoryIdentityProvider in an unhealthy state. + ActiveDirectoryPhaseError ActiveDirectoryIdentityProviderPhase = "Error" +) + +// Status of an Active Directory identity provider. +type ActiveDirectoryIdentityProviderStatus struct { + // Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase ActiveDirectoryIdentityProviderPhase `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 ActiveDirectoryIdentityProviderBind struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an Active Directory 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 (distinguished name) 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 ActiveDirectoryIdentityProviderUserSearchAttributes struct { + // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // Optional, when empty this defaults to "sAMAccountName". + // +optional + Username string `json:"username,omitempty"` + + // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely + // identify the user within this ActiveDirectory provider after a successful authentication. + // Optional, when empty this defaults to "objectGUID". + // +optional + UID string `json:"uid,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name + // in the user's list of groups after a successful authentication. + // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + +type ActiveDirectoryIdentityProviderUserSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for users. + // E.g. "ou=users,dc=example,dc=com". + // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is + // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter at least once 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 ActiveDirectory 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. + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as + // the result of the user search. + // +optional + Attributes ActiveDirectoryIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. + // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and + // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. + // The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the + // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or + // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as + // the result of the group search. + // +optional + Attributes ActiveDirectoryIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + +// Spec for configuring an ActiveDirectory identity provider. +type ActiveDirectoryIdentityProviderSpec struct { + // Host is the hostname of this Active Directory 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 *TLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind ActiveDirectoryIdentityProviderBind `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in Active Directory. + UserSearch ActiveDirectoryIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. + GroupSearch ActiveDirectoryIdentityProviderGroupSearch `json:"groupSearch,omitempty"` +} + +// ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory 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 ActiveDirectoryIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec ActiveDirectoryIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status ActiveDirectoryIdentityProviderStatus `json:"status,omitempty"` +} + +// List of ActiveDirectoryIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ActiveDirectoryIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ActiveDirectoryIdentityProvider `json:"items"` +} diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_meta.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_meta.go index e59976ff3..e04c6f2ae 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_meta.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_meta.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 v1alpha1 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 f7762b09b..9895a76e2 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 @@ -11,6 +11,196 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProvider) DeepCopyInto(out *ActiveDirectoryIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProvider. +func (in *ActiveDirectoryIdentityProvider) DeepCopy() *ActiveDirectoryIdentityProvider { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProvider) 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 *ActiveDirectoryIdentityProviderBind) DeepCopyInto(out *ActiveDirectoryIdentityProviderBind) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderBind. +func (in *ActiveDirectoryIdentityProviderBind) DeepCopy() *ActiveDirectoryIdentityProviderBind { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderBind) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearch. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearchAttributes. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderList) DeepCopyInto(out *ActiveDirectoryIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ActiveDirectoryIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderList. +func (in *ActiveDirectoryIdentityProviderList) DeepCopy() *ActiveDirectoryIdentityProviderList { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProviderList) 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 *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectoryIdentityProviderSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderSpec. +func (in *ActiveDirectoryIdentityProviderSpec) DeepCopy() *ActiveDirectoryIdentityProviderSpec { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopyInto(out *ActiveDirectoryIdentityProviderStatus) { + *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 +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderStatus. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopy() *ActiveDirectoryIdentityProviderStatus { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearch. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopy() *ActiveDirectoryIdentityProviderUserSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearchAttributes. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in diff --git a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..bfcd3c102 --- /dev/null +++ b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProvidersGetter has a method to return a ActiveDirectoryIdentityProviderInterface. +// A group's client should implement this interface. +type ActiveDirectoryIdentityProvidersGetter interface { + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface +} + +// ActiveDirectoryIdentityProviderInterface has methods to work with ActiveDirectoryIdentityProvider resources. +type ActiveDirectoryIdentityProviderInterface interface { + Create(*v1alpha1.ActiveDirectoryIdentityProvider) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + Update(*v1alpha1.ActiveDirectoryIdentityProvider) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + UpdateStatus(*v1alpha1.ActiveDirectoryIdentityProvider) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + List(opts v1.ListOptions) (*v1alpha1.ActiveDirectoryIdentityProviderList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) + ActiveDirectoryIdentityProviderExpansion +} + +// activeDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type activeDirectoryIdentityProviders struct { + client rest.Interface + ns string +} + +// newActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviders +func newActiveDirectoryIdentityProviders(c *IDPV1alpha1Client, namespace string) *activeDirectoryIdentityProviders { + return &activeDirectoryIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *activeDirectoryIdentityProviders) Get(name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *activeDirectoryIdentityProviders) List(opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.ActiveDirectoryIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested activeDirectoryIdentityProviders. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch() +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Create(activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Body(activeDirectoryIdentityProvider). + Do(). + Into(result) + return +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Update(activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + Body(activeDirectoryIdentityProvider). + 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 *activeDirectoryIdentityProviders) UpdateStatus(activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + SubResource("status"). + Body(activeDirectoryIdentityProvider). + Do(). + Into(result) + return +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *activeDirectoryIdentityProviders) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Timeout(timeout). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *activeDirectoryIdentityProviders) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go new file mode 100644 index 000000000..0b3842a0e --- /dev/null +++ b/generated/1.17/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.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" +) + +// FakeActiveDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type FakeActiveDirectoryIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var activedirectoryidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "activedirectoryidentityproviders"} + +var activedirectoryidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "ActiveDirectoryIdentityProvider"} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Get(name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *FakeActiveDirectoryIdentityProviders) List(opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(activedirectoryidentityprovidersResource, activedirectoryidentityprovidersKind, c.ns, opts), &v1alpha1.ActiveDirectoryIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ActiveDirectoryIdentityProviderList{ListMeta: obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).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 activeDirectoryIdentityProviders. +func (c *FakeActiveDirectoryIdentityProviders) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(activedirectoryidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Create(activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Update(activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), 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 *FakeActiveDirectoryIdentityProviders) UpdateStatus(activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(activedirectoryidentityprovidersResource, "status", c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeActiveDirectoryIdentityProviders) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeActiveDirectoryIdentityProviders) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(activedirectoryidentityprovidersResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.ActiveDirectoryIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *FakeActiveDirectoryIdentityProviders) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(activedirectoryidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} 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 23859d838..4879c13ea 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) ActiveDirectoryIdentityProviders(namespace string) v1alpha1.ActiveDirectoryIdentityProviderInterface { + return &FakeActiveDirectoryIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } 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 137892f3b..a7fdb511d 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,6 +5,8 @@ package v1alpha1 +type ActiveDirectoryIdentityProviderExpansion interface{} + 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 9176e7522..bcae95d95 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 + ActiveDirectoryIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -22,6 +23,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface { + return newActiveDirectoryIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.17/client/supervisor/informers/externalversions/generic.go b/generated/1.17/client/supervisor/informers/externalversions/generic.go index f65c952df..b3f29c6ac 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("activedirectoryidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..4757c8aec --- /dev/null +++ b/generated/1.17/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderInformer provides access to a shared informer and lister for +// ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ActiveDirectoryIdentityProviderLister +} + +type activeDirectoryIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewActiveDirectoryIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewFilteredActiveDirectoryIdentityProviderInformer(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().ActiveDirectoryIdentityProviders(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().ActiveDirectoryIdentityProviders(namespace).Watch(options) + }, + }, + &idpv1alpha1.ActiveDirectoryIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *activeDirectoryIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *activeDirectoryIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.ActiveDirectoryIdentityProvider{}, f.defaultInformer) +} + +func (f *activeDirectoryIdentityProviderInformer) Lister() v1alpha1.ActiveDirectoryIdentityProviderLister { + return v1alpha1.NewActiveDirectoryIdentityProviderLister(f.Informer().GetIndexer()) +} 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 b7677ddb2..4e29527ee 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 { + // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. + ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -28,6 +30,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. +func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer { + return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.17/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.17/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..7dbde1729 --- /dev/null +++ b/generated/1.17/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderLister helps list ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister + ActiveDirectoryIdentityProviderListerExpansion +} + +// activeDirectoryIdentityProviderLister implements the ActiveDirectoryIdentityProviderLister interface. +type activeDirectoryIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewActiveDirectoryIdentityProviderLister returns a new ActiveDirectoryIdentityProviderLister. +func NewActiveDirectoryIdentityProviderLister(indexer cache.Indexer) ActiveDirectoryIdentityProviderLister { + return &activeDirectoryIdentityProviderLister{indexer: indexer} +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer. +func (s *activeDirectoryIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. +func (s *activeDirectoryIdentityProviderLister) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister { + return activeDirectoryIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ActiveDirectoryIdentityProviderNamespaceLister helps list and get ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderNamespaceLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + ActiveDirectoryIdentityProviderNamespaceListerExpansion +} + +// activeDirectoryIdentityProviderNamespaceLister implements the ActiveDirectoryIdentityProviderNamespaceLister +// interface. +type activeDirectoryIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. +func (s activeDirectoryIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. +func (s activeDirectoryIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("activedirectoryidentityprovider"), name) + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), nil +} 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 28f41bd77..7c625e804 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 +// ActiveDirectoryIdentityProviderListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderLister. +type ActiveDirectoryIdentityProviderListerExpansion interface{} + +// ActiveDirectoryIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderNamespaceLister. +type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml new file mode 100644 index 000000000..03fbcd085 --- /dev/null +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -0,0 +1,271 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: ActiveDirectoryIdentityProvider + listKind: ActiveDirectoryIdentityProviderList + plural: activedirectoryidentityproviders + singular: activedirectoryidentityprovider + 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: ActiveDirectoryIdentityProvider describes the configuration of + an upstream Microsoft Active Directory 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: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the ActiveDirectory 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 + Active Directory 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 (distinguished name) 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: + - secretName + type: object + groupSearch: + description: GroupSearch contains the configuration for searching + for a user's group membership in ActiveDirectory. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each ActiveDirectory entry which was found + as the result of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the Active Directory entries whose value shall become + a group name in the user's list of groups after a successful + authentication. The value of this field is case-sensitive + and must match the case of the attribute name returned by + the ActiveDirectory server in the user's entry. E.g. "cn" + for common name. Distinguished names can be used by specifying + lower-case "dn". Optional. When not specified, the default + will act as if the GroupName were specified as "dn" (distinguished + name). + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". + When not specified, no group search will be performed and authenticated + users will not belong to any groups from the ActiveDirectory + provider. Also, when not specified, the values of Filter and + Attributes are ignored. + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for groups for a user. The + pattern "{}" must occur in the filter at least once and will + be dynamically replaced by the dn (distinguished name) of the + user entry found as a result of the user search. E.g. "member={}" + or "&(objectClass=groupOfNames)(member={})". For more information + about ActiveDirectory 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 "member={}". + type: string + type: object + host: + description: 'Host is the hostname of this Active Directory 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). + 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 Active Directory. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the ActiveDirectory entry which was found as the + result of the user search. + properties: + uid: + description: UID specifies the name of the attribute in the + ActiveDirectory entry which whose value shall be used to + uniquely identify the user within this ActiveDirectory provider + after a successful authentication. Optional, when empty + this defaults to "objectGUID". + type: string + username: + description: Username specifies the name of the attribute + in the ActiveDirectory entry whose value shall become the + username of the user after a successful authentication. + This would typically be the same attribute name used in + Optional, when empty this defaults to "sAMAccountName". + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + Optional, when not specified it will be defaulted based on the + host, for example if your active directory host is "activedirectory.example.com:636", + it will be "dc=activedirectory,dc=example,dc=com". + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for users. The pattern "{}" + must occur in the filter at least once 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 ActiveDirectory 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. + type: string + type: object + required: + - host + type: object + 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 ActiveDirectoryIdentityProvider. + 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 ade6ea1b2..12946f740 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -748,6 +748,157 @@ 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-activedirectoryidentityprovider"] +==== ActiveDirectoryIdentityProvider + +ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderlist[$$ActiveDirectoryIdentityProviderList$$] +**** + +[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-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderbind"] +==== ActiveDirectoryIdentityProviderBind + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[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 Active Directory 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 (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch"] +==== ActiveDirectoryIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes"] +==== ActiveDirectoryIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec"] +==== ActiveDirectoryIdentityProviderSpec + +Spec for configuring an ActiveDirectory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this Active Directory 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-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-activedirectoryidentityproviderbind[$$ActiveDirectoryIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory 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-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in Active Directory. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus"] +==== ActiveDirectoryIdentityProviderStatus + +Status of an Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __ActiveDirectoryIdentityProviderPhase__ | Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch"] +==== ActiveDirectoryIdentityProviderUserSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be defaulted based on the host, for example if your active directory host is "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes"] +==== ActiveDirectoryIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-condition"] ==== Condition @@ -755,6 +906,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity pro .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$] - 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$$] **** @@ -1054,6 +1206,7 @@ Status of an OIDC identity provider. .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] - 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/register.go b/generated/1.18/apis/supervisor/idp/v1alpha1/register.go index ddc9c3607..e3406a814 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/register.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 v1alpha1 @@ -34,6 +34,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &OIDCIdentityProviderList{}, &LDAPIdentityProvider{}, &LDAPIdentityProviderList{}, + &ActiveDirectoryIdentityProvider{}, + &ActiveDirectoryIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go new file mode 100644 index 000000000..18726e7bd --- /dev/null +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -0,0 +1,167 @@ +// 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 ActiveDirectoryIdentityProviderPhase string + +const ( + // ActiveDirectoryPhasePending is the default phase for newly-created ActiveDirectoryIdentityProvider resources. + ActiveDirectoryPhasePending ActiveDirectoryIdentityProviderPhase = "Pending" + + // ActiveDirectoryPhaseReady is the phase for an ActiveDirectoryIdentityProvider resource in a healthy state. + ActiveDirectoryPhaseReady ActiveDirectoryIdentityProviderPhase = "Ready" + + // ActiveDirectoryPhaseError is the phase for an ActiveDirectoryIdentityProvider in an unhealthy state. + ActiveDirectoryPhaseError ActiveDirectoryIdentityProviderPhase = "Error" +) + +// Status of an Active Directory identity provider. +type ActiveDirectoryIdentityProviderStatus struct { + // Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase ActiveDirectoryIdentityProviderPhase `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 ActiveDirectoryIdentityProviderBind struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an Active Directory 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 (distinguished name) 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 ActiveDirectoryIdentityProviderUserSearchAttributes struct { + // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // Optional, when empty this defaults to "sAMAccountName". + // +optional + Username string `json:"username,omitempty"` + + // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely + // identify the user within this ActiveDirectory provider after a successful authentication. + // Optional, when empty this defaults to "objectGUID". + // +optional + UID string `json:"uid,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name + // in the user's list of groups after a successful authentication. + // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + +type ActiveDirectoryIdentityProviderUserSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for users. + // E.g. "ou=users,dc=example,dc=com". + // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is + // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter at least once 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 ActiveDirectory 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. + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as + // the result of the user search. + // +optional + Attributes ActiveDirectoryIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. + // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and + // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. + // The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the + // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or + // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as + // the result of the group search. + // +optional + Attributes ActiveDirectoryIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + +// Spec for configuring an ActiveDirectory identity provider. +type ActiveDirectoryIdentityProviderSpec struct { + // Host is the hostname of this Active Directory 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 *TLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind ActiveDirectoryIdentityProviderBind `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in Active Directory. + UserSearch ActiveDirectoryIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. + GroupSearch ActiveDirectoryIdentityProviderGroupSearch `json:"groupSearch,omitempty"` +} + +// ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory 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 ActiveDirectoryIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec ActiveDirectoryIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status ActiveDirectoryIdentityProviderStatus `json:"status,omitempty"` +} + +// List of ActiveDirectoryIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ActiveDirectoryIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ActiveDirectoryIdentityProvider `json:"items"` +} diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_meta.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_meta.go index e59976ff3..e04c6f2ae 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_meta.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_meta.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 v1alpha1 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 f7762b09b..9895a76e2 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 @@ -11,6 +11,196 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProvider) DeepCopyInto(out *ActiveDirectoryIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProvider. +func (in *ActiveDirectoryIdentityProvider) DeepCopy() *ActiveDirectoryIdentityProvider { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProvider) 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 *ActiveDirectoryIdentityProviderBind) DeepCopyInto(out *ActiveDirectoryIdentityProviderBind) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderBind. +func (in *ActiveDirectoryIdentityProviderBind) DeepCopy() *ActiveDirectoryIdentityProviderBind { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderBind) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearch. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearchAttributes. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderList) DeepCopyInto(out *ActiveDirectoryIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ActiveDirectoryIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderList. +func (in *ActiveDirectoryIdentityProviderList) DeepCopy() *ActiveDirectoryIdentityProviderList { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProviderList) 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 *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectoryIdentityProviderSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderSpec. +func (in *ActiveDirectoryIdentityProviderSpec) DeepCopy() *ActiveDirectoryIdentityProviderSpec { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopyInto(out *ActiveDirectoryIdentityProviderStatus) { + *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 +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderStatus. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopy() *ActiveDirectoryIdentityProviderStatus { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearch. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopy() *ActiveDirectoryIdentityProviderUserSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearchAttributes. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in diff --git a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..5d2cd3ca6 --- /dev/null +++ b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProvidersGetter has a method to return a ActiveDirectoryIdentityProviderInterface. +// A group's client should implement this interface. +type ActiveDirectoryIdentityProvidersGetter interface { + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface +} + +// ActiveDirectoryIdentityProviderInterface has methods to work with ActiveDirectoryIdentityProvider resources. +type ActiveDirectoryIdentityProviderInterface interface { + Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, 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.ActiveDirectoryIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ActiveDirectoryIdentityProviderList, 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.ActiveDirectoryIdentityProvider, err error) + ActiveDirectoryIdentityProviderExpansion +} + +// activeDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type activeDirectoryIdentityProviders struct { + client rest.Interface + ns string +} + +// newActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviders +func newActiveDirectoryIdentityProviders(c *IDPV1alpha1Client, namespace string) *activeDirectoryIdentityProviders { + return &activeDirectoryIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *activeDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *activeDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.ActiveDirectoryIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested activeDirectoryIdentityProviders. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + 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 *activeDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *activeDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *activeDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go new file mode 100644 index 000000000..d3f12004d --- /dev/null +++ b/generated/1.18/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.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" +) + +// FakeActiveDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type FakeActiveDirectoryIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var activedirectoryidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "activedirectoryidentityproviders"} + +var activedirectoryidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "ActiveDirectoryIdentityProvider"} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *FakeActiveDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(activedirectoryidentityprovidersResource, activedirectoryidentityprovidersKind, c.ns, opts), &v1alpha1.ActiveDirectoryIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ActiveDirectoryIdentityProviderList{ListMeta: obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).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 activeDirectoryIdentityProviders. +func (c *FakeActiveDirectoryIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(activedirectoryidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), 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 *FakeActiveDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(activedirectoryidentityprovidersResource, "status", c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeActiveDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeActiveDirectoryIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(activedirectoryidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.ActiveDirectoryIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *FakeActiveDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(activedirectoryidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} 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 2c419cebf..669a43cb5 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) ActiveDirectoryIdentityProviders(namespace string) v1alpha1.ActiveDirectoryIdentityProviderInterface { + return &FakeActiveDirectoryIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } 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 137892f3b..a7fdb511d 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,6 +5,8 @@ package v1alpha1 +type ActiveDirectoryIdentityProviderExpansion interface{} + 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 fdb10351b..208b45439 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 + ActiveDirectoryIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -22,6 +23,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface { + return newActiveDirectoryIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.18/client/supervisor/informers/externalversions/generic.go b/generated/1.18/client/supervisor/informers/externalversions/generic.go index 9d15fcd24..9a2e98537 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("activedirectoryidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..54647c94b --- /dev/null +++ b/generated/1.18/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderInformer provides access to a shared informer and lister for +// ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ActiveDirectoryIdentityProviderLister +} + +type activeDirectoryIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewActiveDirectoryIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewFilteredActiveDirectoryIdentityProviderInformer(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().ActiveDirectoryIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().ActiveDirectoryIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.ActiveDirectoryIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *activeDirectoryIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *activeDirectoryIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.ActiveDirectoryIdentityProvider{}, f.defaultInformer) +} + +func (f *activeDirectoryIdentityProviderInformer) Lister() v1alpha1.ActiveDirectoryIdentityProviderLister { + return v1alpha1.NewActiveDirectoryIdentityProviderLister(f.Informer().GetIndexer()) +} 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 009ad89c6..9456d5e25 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 { + // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. + ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -28,6 +30,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. +func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer { + return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.18/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.18/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..910257643 --- /dev/null +++ b/generated/1.18/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderLister helps list ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister + ActiveDirectoryIdentityProviderListerExpansion +} + +// activeDirectoryIdentityProviderLister implements the ActiveDirectoryIdentityProviderLister interface. +type activeDirectoryIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewActiveDirectoryIdentityProviderLister returns a new ActiveDirectoryIdentityProviderLister. +func NewActiveDirectoryIdentityProviderLister(indexer cache.Indexer) ActiveDirectoryIdentityProviderLister { + return &activeDirectoryIdentityProviderLister{indexer: indexer} +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer. +func (s *activeDirectoryIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. +func (s *activeDirectoryIdentityProviderLister) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister { + return activeDirectoryIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ActiveDirectoryIdentityProviderNamespaceLister helps list and get ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderNamespaceLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + ActiveDirectoryIdentityProviderNamespaceListerExpansion +} + +// activeDirectoryIdentityProviderNamespaceLister implements the ActiveDirectoryIdentityProviderNamespaceLister +// interface. +type activeDirectoryIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. +func (s activeDirectoryIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. +func (s activeDirectoryIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("activedirectoryidentityprovider"), name) + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), nil +} 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 28f41bd77..7c625e804 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 +// ActiveDirectoryIdentityProviderListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderLister. +type ActiveDirectoryIdentityProviderListerExpansion interface{} + +// ActiveDirectoryIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderNamespaceLister. +type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml new file mode 100644 index 000000000..03fbcd085 --- /dev/null +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -0,0 +1,271 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: ActiveDirectoryIdentityProvider + listKind: ActiveDirectoryIdentityProviderList + plural: activedirectoryidentityproviders + singular: activedirectoryidentityprovider + 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: ActiveDirectoryIdentityProvider describes the configuration of + an upstream Microsoft Active Directory 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: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the ActiveDirectory 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 + Active Directory 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 (distinguished name) 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: + - secretName + type: object + groupSearch: + description: GroupSearch contains the configuration for searching + for a user's group membership in ActiveDirectory. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each ActiveDirectory entry which was found + as the result of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the Active Directory entries whose value shall become + a group name in the user's list of groups after a successful + authentication. The value of this field is case-sensitive + and must match the case of the attribute name returned by + the ActiveDirectory server in the user's entry. E.g. "cn" + for common name. Distinguished names can be used by specifying + lower-case "dn". Optional. When not specified, the default + will act as if the GroupName were specified as "dn" (distinguished + name). + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". + When not specified, no group search will be performed and authenticated + users will not belong to any groups from the ActiveDirectory + provider. Also, when not specified, the values of Filter and + Attributes are ignored. + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for groups for a user. The + pattern "{}" must occur in the filter at least once and will + be dynamically replaced by the dn (distinguished name) of the + user entry found as a result of the user search. E.g. "member={}" + or "&(objectClass=groupOfNames)(member={})". For more information + about ActiveDirectory 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 "member={}". + type: string + type: object + host: + description: 'Host is the hostname of this Active Directory 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). + 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 Active Directory. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the ActiveDirectory entry which was found as the + result of the user search. + properties: + uid: + description: UID specifies the name of the attribute in the + ActiveDirectory entry which whose value shall be used to + uniquely identify the user within this ActiveDirectory provider + after a successful authentication. Optional, when empty + this defaults to "objectGUID". + type: string + username: + description: Username specifies the name of the attribute + in the ActiveDirectory entry whose value shall become the + username of the user after a successful authentication. + This would typically be the same attribute name used in + Optional, when empty this defaults to "sAMAccountName". + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + Optional, when not specified it will be defaulted based on the + host, for example if your active directory host is "activedirectory.example.com:636", + it will be "dc=activedirectory,dc=example,dc=com". + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for users. The pattern "{}" + must occur in the filter at least once 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 ActiveDirectory 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. + type: string + type: object + required: + - host + type: object + 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 ActiveDirectoryIdentityProvider. + 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 7f47bdeb1..eadb1b33f 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -748,6 +748,157 @@ 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-activedirectoryidentityprovider"] +==== ActiveDirectoryIdentityProvider + +ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderlist[$$ActiveDirectoryIdentityProviderList$$] +**** + +[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-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderbind"] +==== ActiveDirectoryIdentityProviderBind + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[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 Active Directory 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 (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch"] +==== ActiveDirectoryIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes"] +==== ActiveDirectoryIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec"] +==== ActiveDirectoryIdentityProviderSpec + +Spec for configuring an ActiveDirectory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this Active Directory 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-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-activedirectoryidentityproviderbind[$$ActiveDirectoryIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory 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-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in Active Directory. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus"] +==== ActiveDirectoryIdentityProviderStatus + +Status of an Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __ActiveDirectoryIdentityProviderPhase__ | Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch"] +==== ActiveDirectoryIdentityProviderUserSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be defaulted based on the host, for example if your active directory host is "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes"] +==== ActiveDirectoryIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-condition"] ==== Condition @@ -755,6 +906,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity pro .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$] - 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$$] **** @@ -1054,6 +1206,7 @@ Status of an OIDC identity provider. .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] - 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/register.go b/generated/1.19/apis/supervisor/idp/v1alpha1/register.go index ddc9c3607..e3406a814 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/register.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 v1alpha1 @@ -34,6 +34,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &OIDCIdentityProviderList{}, &LDAPIdentityProvider{}, &LDAPIdentityProviderList{}, + &ActiveDirectoryIdentityProvider{}, + &ActiveDirectoryIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go new file mode 100644 index 000000000..18726e7bd --- /dev/null +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -0,0 +1,167 @@ +// 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 ActiveDirectoryIdentityProviderPhase string + +const ( + // ActiveDirectoryPhasePending is the default phase for newly-created ActiveDirectoryIdentityProvider resources. + ActiveDirectoryPhasePending ActiveDirectoryIdentityProviderPhase = "Pending" + + // ActiveDirectoryPhaseReady is the phase for an ActiveDirectoryIdentityProvider resource in a healthy state. + ActiveDirectoryPhaseReady ActiveDirectoryIdentityProviderPhase = "Ready" + + // ActiveDirectoryPhaseError is the phase for an ActiveDirectoryIdentityProvider in an unhealthy state. + ActiveDirectoryPhaseError ActiveDirectoryIdentityProviderPhase = "Error" +) + +// Status of an Active Directory identity provider. +type ActiveDirectoryIdentityProviderStatus struct { + // Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase ActiveDirectoryIdentityProviderPhase `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 ActiveDirectoryIdentityProviderBind struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an Active Directory 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 (distinguished name) 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 ActiveDirectoryIdentityProviderUserSearchAttributes struct { + // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // Optional, when empty this defaults to "sAMAccountName". + // +optional + Username string `json:"username,omitempty"` + + // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely + // identify the user within this ActiveDirectory provider after a successful authentication. + // Optional, when empty this defaults to "objectGUID". + // +optional + UID string `json:"uid,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name + // in the user's list of groups after a successful authentication. + // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + +type ActiveDirectoryIdentityProviderUserSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for users. + // E.g. "ou=users,dc=example,dc=com". + // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is + // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter at least once 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 ActiveDirectory 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. + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as + // the result of the user search. + // +optional + Attributes ActiveDirectoryIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. + // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and + // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. + // The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the + // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or + // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as + // the result of the group search. + // +optional + Attributes ActiveDirectoryIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + +// Spec for configuring an ActiveDirectory identity provider. +type ActiveDirectoryIdentityProviderSpec struct { + // Host is the hostname of this Active Directory 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 *TLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind ActiveDirectoryIdentityProviderBind `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in Active Directory. + UserSearch ActiveDirectoryIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. + GroupSearch ActiveDirectoryIdentityProviderGroupSearch `json:"groupSearch,omitempty"` +} + +// ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory 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 ActiveDirectoryIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec ActiveDirectoryIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status ActiveDirectoryIdentityProviderStatus `json:"status,omitempty"` +} + +// List of ActiveDirectoryIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ActiveDirectoryIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ActiveDirectoryIdentityProvider `json:"items"` +} diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_meta.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_meta.go index e59976ff3..e04c6f2ae 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_meta.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_meta.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 v1alpha1 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 f7762b09b..9895a76e2 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 @@ -11,6 +11,196 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProvider) DeepCopyInto(out *ActiveDirectoryIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProvider. +func (in *ActiveDirectoryIdentityProvider) DeepCopy() *ActiveDirectoryIdentityProvider { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProvider) 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 *ActiveDirectoryIdentityProviderBind) DeepCopyInto(out *ActiveDirectoryIdentityProviderBind) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderBind. +func (in *ActiveDirectoryIdentityProviderBind) DeepCopy() *ActiveDirectoryIdentityProviderBind { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderBind) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearch. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearchAttributes. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderList) DeepCopyInto(out *ActiveDirectoryIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ActiveDirectoryIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderList. +func (in *ActiveDirectoryIdentityProviderList) DeepCopy() *ActiveDirectoryIdentityProviderList { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProviderList) 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 *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectoryIdentityProviderSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderSpec. +func (in *ActiveDirectoryIdentityProviderSpec) DeepCopy() *ActiveDirectoryIdentityProviderSpec { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopyInto(out *ActiveDirectoryIdentityProviderStatus) { + *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 +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderStatus. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopy() *ActiveDirectoryIdentityProviderStatus { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearch. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopy() *ActiveDirectoryIdentityProviderUserSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearchAttributes. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in diff --git a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..bf5852588 --- /dev/null +++ b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProvidersGetter has a method to return a ActiveDirectoryIdentityProviderInterface. +// A group's client should implement this interface. +type ActiveDirectoryIdentityProvidersGetter interface { + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface +} + +// ActiveDirectoryIdentityProviderInterface has methods to work with ActiveDirectoryIdentityProvider resources. +type ActiveDirectoryIdentityProviderInterface interface { + Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, 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.ActiveDirectoryIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ActiveDirectoryIdentityProviderList, 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.ActiveDirectoryIdentityProvider, err error) + ActiveDirectoryIdentityProviderExpansion +} + +// activeDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type activeDirectoryIdentityProviders struct { + client rest.Interface + ns string +} + +// newActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviders +func newActiveDirectoryIdentityProviders(c *IDPV1alpha1Client, namespace string) *activeDirectoryIdentityProviders { + return &activeDirectoryIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *activeDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *activeDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.ActiveDirectoryIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested activeDirectoryIdentityProviders. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + 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 *activeDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *activeDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *activeDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go new file mode 100644 index 000000000..696aab0ee --- /dev/null +++ b/generated/1.19/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.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" +) + +// FakeActiveDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type FakeActiveDirectoryIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var activedirectoryidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "activedirectoryidentityproviders"} + +var activedirectoryidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "ActiveDirectoryIdentityProvider"} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *FakeActiveDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(activedirectoryidentityprovidersResource, activedirectoryidentityprovidersKind, c.ns, opts), &v1alpha1.ActiveDirectoryIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ActiveDirectoryIdentityProviderList{ListMeta: obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).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 activeDirectoryIdentityProviders. +func (c *FakeActiveDirectoryIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(activedirectoryidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), 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 *FakeActiveDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(activedirectoryidentityprovidersResource, "status", c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeActiveDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeActiveDirectoryIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(activedirectoryidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.ActiveDirectoryIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *FakeActiveDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(activedirectoryidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} 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 28e3d63f3..ea0ea1952 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) ActiveDirectoryIdentityProviders(namespace string) v1alpha1.ActiveDirectoryIdentityProviderInterface { + return &FakeActiveDirectoryIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } 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 137892f3b..a7fdb511d 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,6 +5,8 @@ package v1alpha1 +type ActiveDirectoryIdentityProviderExpansion interface{} + 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 213c06015..bfdf55a3c 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 + ActiveDirectoryIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -22,6 +23,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface { + return newActiveDirectoryIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.19/client/supervisor/informers/externalversions/generic.go b/generated/1.19/client/supervisor/informers/externalversions/generic.go index 8308f3a97..77b758f2e 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("activedirectoryidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..6601bd4b6 --- /dev/null +++ b/generated/1.19/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderInformer provides access to a shared informer and lister for +// ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ActiveDirectoryIdentityProviderLister +} + +type activeDirectoryIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewActiveDirectoryIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewFilteredActiveDirectoryIdentityProviderInformer(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().ActiveDirectoryIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().ActiveDirectoryIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.ActiveDirectoryIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *activeDirectoryIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *activeDirectoryIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.ActiveDirectoryIdentityProvider{}, f.defaultInformer) +} + +func (f *activeDirectoryIdentityProviderInformer) Lister() v1alpha1.ActiveDirectoryIdentityProviderLister { + return v1alpha1.NewActiveDirectoryIdentityProviderLister(f.Informer().GetIndexer()) +} 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 1b3a9f24d..dda26a61e 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 { + // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. + ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -28,6 +30,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. +func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer { + return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.19/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.19/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..21cbeee52 --- /dev/null +++ b/generated/1.19/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderLister helps list ActiveDirectoryIdentityProviders. +// All objects returned here must be treated as read-only. +type ActiveDirectoryIdentityProviderLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister + ActiveDirectoryIdentityProviderListerExpansion +} + +// activeDirectoryIdentityProviderLister implements the ActiveDirectoryIdentityProviderLister interface. +type activeDirectoryIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewActiveDirectoryIdentityProviderLister returns a new ActiveDirectoryIdentityProviderLister. +func NewActiveDirectoryIdentityProviderLister(indexer cache.Indexer) ActiveDirectoryIdentityProviderLister { + return &activeDirectoryIdentityProviderLister{indexer: indexer} +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer. +func (s *activeDirectoryIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. +func (s *activeDirectoryIdentityProviderLister) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister { + return activeDirectoryIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ActiveDirectoryIdentityProviderNamespaceLister helps list and get ActiveDirectoryIdentityProviders. +// All objects returned here must be treated as read-only. +type ActiveDirectoryIdentityProviderNamespaceLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + ActiveDirectoryIdentityProviderNamespaceListerExpansion +} + +// activeDirectoryIdentityProviderNamespaceLister implements the ActiveDirectoryIdentityProviderNamespaceLister +// interface. +type activeDirectoryIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. +func (s activeDirectoryIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. +func (s activeDirectoryIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("activedirectoryidentityprovider"), name) + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), nil +} 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 28f41bd77..7c625e804 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 +// ActiveDirectoryIdentityProviderListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderLister. +type ActiveDirectoryIdentityProviderListerExpansion interface{} + +// ActiveDirectoryIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderNamespaceLister. +type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml new file mode 100644 index 000000000..03fbcd085 --- /dev/null +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -0,0 +1,271 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: ActiveDirectoryIdentityProvider + listKind: ActiveDirectoryIdentityProviderList + plural: activedirectoryidentityproviders + singular: activedirectoryidentityprovider + 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: ActiveDirectoryIdentityProvider describes the configuration of + an upstream Microsoft Active Directory 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: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the ActiveDirectory 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 + Active Directory 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 (distinguished name) 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: + - secretName + type: object + groupSearch: + description: GroupSearch contains the configuration for searching + for a user's group membership in ActiveDirectory. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each ActiveDirectory entry which was found + as the result of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the Active Directory entries whose value shall become + a group name in the user's list of groups after a successful + authentication. The value of this field is case-sensitive + and must match the case of the attribute name returned by + the ActiveDirectory server in the user's entry. E.g. "cn" + for common name. Distinguished names can be used by specifying + lower-case "dn". Optional. When not specified, the default + will act as if the GroupName were specified as "dn" (distinguished + name). + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". + When not specified, no group search will be performed and authenticated + users will not belong to any groups from the ActiveDirectory + provider. Also, when not specified, the values of Filter and + Attributes are ignored. + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for groups for a user. The + pattern "{}" must occur in the filter at least once and will + be dynamically replaced by the dn (distinguished name) of the + user entry found as a result of the user search. E.g. "member={}" + or "&(objectClass=groupOfNames)(member={})". For more information + about ActiveDirectory 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 "member={}". + type: string + type: object + host: + description: 'Host is the hostname of this Active Directory 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). + 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 Active Directory. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the ActiveDirectory entry which was found as the + result of the user search. + properties: + uid: + description: UID specifies the name of the attribute in the + ActiveDirectory entry which whose value shall be used to + uniquely identify the user within this ActiveDirectory provider + after a successful authentication. Optional, when empty + this defaults to "objectGUID". + type: string + username: + description: Username specifies the name of the attribute + in the ActiveDirectory entry whose value shall become the + username of the user after a successful authentication. + This would typically be the same attribute name used in + Optional, when empty this defaults to "sAMAccountName". + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + Optional, when not specified it will be defaulted based on the + host, for example if your active directory host is "activedirectory.example.com:636", + it will be "dc=activedirectory,dc=example,dc=com". + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for users. The pattern "{}" + must occur in the filter at least once 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 ActiveDirectory 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. + type: string + type: object + required: + - host + type: object + 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 ActiveDirectoryIdentityProvider. + 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 1bcf7d081..130514528 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -748,6 +748,157 @@ 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-activedirectoryidentityprovider"] +==== ActiveDirectoryIdentityProvider + +ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderlist[$$ActiveDirectoryIdentityProviderList$$] +**** + +[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-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$]__ | Spec for configuring the identity provider. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$]__ | Status of the identity provider. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderbind"] +==== ActiveDirectoryIdentityProviderBind + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[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 Active Directory 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 (distinguished name) of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch"] +==== ActiveDirectoryIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes"] +==== ActiveDirectoryIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +|=== + + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec"] +==== ActiveDirectoryIdentityProviderSpec + +Spec for configuring an ActiveDirectory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`host`* __string__ | Host is the hostname of this Active Directory 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-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-activedirectoryidentityproviderbind[$$ActiveDirectoryIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory 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-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in Active Directory. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearch[$$ActiveDirectoryIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus"] +==== ActiveDirectoryIdentityProviderStatus + +Status of an Active Directory identity provider. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovider[$$ActiveDirectoryIdentityProvider$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __ActiveDirectoryIdentityProviderPhase__ | Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. +| *`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. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch"] +==== ActiveDirectoryIdentityProviderUserSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be defaulted based on the host, for example if your active directory host is "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes"] +==== ActiveDirectoryIdentityProviderUserSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearch[$$ActiveDirectoryIdentityProviderUserSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`username`* __string__ | Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-condition"] ==== Condition @@ -755,6 +906,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor identity pro .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderstatus[$$ActiveDirectoryIdentityProviderStatus$$] - 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$$] **** @@ -1054,6 +1206,7 @@ Status of an OIDC identity provider. .Appears In: **** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderspec[$$ActiveDirectoryIdentityProviderSpec$$] - 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/register.go b/generated/1.20/apis/supervisor/idp/v1alpha1/register.go index ddc9c3607..e3406a814 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/register.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 v1alpha1 @@ -34,6 +34,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &OIDCIdentityProviderList{}, &LDAPIdentityProvider{}, &LDAPIdentityProviderList{}, + &ActiveDirectoryIdentityProvider{}, + &ActiveDirectoryIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go new file mode 100644 index 000000000..18726e7bd --- /dev/null +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -0,0 +1,167 @@ +// 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 ActiveDirectoryIdentityProviderPhase string + +const ( + // ActiveDirectoryPhasePending is the default phase for newly-created ActiveDirectoryIdentityProvider resources. + ActiveDirectoryPhasePending ActiveDirectoryIdentityProviderPhase = "Pending" + + // ActiveDirectoryPhaseReady is the phase for an ActiveDirectoryIdentityProvider resource in a healthy state. + ActiveDirectoryPhaseReady ActiveDirectoryIdentityProviderPhase = "Ready" + + // ActiveDirectoryPhaseError is the phase for an ActiveDirectoryIdentityProvider in an unhealthy state. + ActiveDirectoryPhaseError ActiveDirectoryIdentityProviderPhase = "Error" +) + +// Status of an Active Directory identity provider. +type ActiveDirectoryIdentityProviderStatus struct { + // Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase ActiveDirectoryIdentityProviderPhase `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 ActiveDirectoryIdentityProviderBind struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an Active Directory 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 (distinguished name) 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 ActiveDirectoryIdentityProviderUserSearchAttributes struct { + // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // Optional, when empty this defaults to "sAMAccountName". + // +optional + Username string `json:"username,omitempty"` + + // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely + // identify the user within this ActiveDirectory provider after a successful authentication. + // Optional, when empty this defaults to "objectGUID". + // +optional + UID string `json:"uid,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name + // in the user's list of groups after a successful authentication. + // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + +type ActiveDirectoryIdentityProviderUserSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for users. + // E.g. "ou=users,dc=example,dc=com". + // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is + // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter at least once 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 ActiveDirectory 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. + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as + // the result of the user search. + // +optional + Attributes ActiveDirectoryIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. + // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and + // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. + // The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the + // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or + // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as + // the result of the group search. + // +optional + Attributes ActiveDirectoryIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + +// Spec for configuring an ActiveDirectory identity provider. +type ActiveDirectoryIdentityProviderSpec struct { + // Host is the hostname of this Active Directory 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 *TLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind ActiveDirectoryIdentityProviderBind `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in Active Directory. + UserSearch ActiveDirectoryIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. + GroupSearch ActiveDirectoryIdentityProviderGroupSearch `json:"groupSearch,omitempty"` +} + +// ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory 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 ActiveDirectoryIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec ActiveDirectoryIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status ActiveDirectoryIdentityProviderStatus `json:"status,omitempty"` +} + +// List of ActiveDirectoryIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ActiveDirectoryIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ActiveDirectoryIdentityProvider `json:"items"` +} diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_meta.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_meta.go index e59976ff3..e04c6f2ae 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_meta.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_meta.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 v1alpha1 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 f7762b09b..9895a76e2 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 @@ -11,6 +11,196 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProvider) DeepCopyInto(out *ActiveDirectoryIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProvider. +func (in *ActiveDirectoryIdentityProvider) DeepCopy() *ActiveDirectoryIdentityProvider { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProvider) 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 *ActiveDirectoryIdentityProviderBind) DeepCopyInto(out *ActiveDirectoryIdentityProviderBind) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderBind. +func (in *ActiveDirectoryIdentityProviderBind) DeepCopy() *ActiveDirectoryIdentityProviderBind { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderBind) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearch. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearchAttributes. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderList) DeepCopyInto(out *ActiveDirectoryIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ActiveDirectoryIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderList. +func (in *ActiveDirectoryIdentityProviderList) DeepCopy() *ActiveDirectoryIdentityProviderList { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProviderList) 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 *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectoryIdentityProviderSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderSpec. +func (in *ActiveDirectoryIdentityProviderSpec) DeepCopy() *ActiveDirectoryIdentityProviderSpec { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopyInto(out *ActiveDirectoryIdentityProviderStatus) { + *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 +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderStatus. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopy() *ActiveDirectoryIdentityProviderStatus { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearch. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopy() *ActiveDirectoryIdentityProviderUserSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearchAttributes. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in diff --git a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..2958cb149 --- /dev/null +++ b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProvidersGetter has a method to return a ActiveDirectoryIdentityProviderInterface. +// A group's client should implement this interface. +type ActiveDirectoryIdentityProvidersGetter interface { + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface +} + +// ActiveDirectoryIdentityProviderInterface has methods to work with ActiveDirectoryIdentityProvider resources. +type ActiveDirectoryIdentityProviderInterface interface { + Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, 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.ActiveDirectoryIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ActiveDirectoryIdentityProviderList, 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.ActiveDirectoryIdentityProvider, err error) + ActiveDirectoryIdentityProviderExpansion +} + +// activeDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type activeDirectoryIdentityProviders struct { + client rest.Interface + ns string +} + +// newActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviders +func newActiveDirectoryIdentityProviders(c *IDPV1alpha1Client, namespace string) *activeDirectoryIdentityProviders { + return &activeDirectoryIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *activeDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *activeDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.ActiveDirectoryIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested activeDirectoryIdentityProviders. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + 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 *activeDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *activeDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *activeDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go new file mode 100644 index 000000000..27ace5a2a --- /dev/null +++ b/generated/1.20/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.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" +) + +// FakeActiveDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type FakeActiveDirectoryIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var activedirectoryidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "activedirectoryidentityproviders"} + +var activedirectoryidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "ActiveDirectoryIdentityProvider"} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *FakeActiveDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(activedirectoryidentityprovidersResource, activedirectoryidentityprovidersKind, c.ns, opts), &v1alpha1.ActiveDirectoryIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ActiveDirectoryIdentityProviderList{ListMeta: obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).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 activeDirectoryIdentityProviders. +func (c *FakeActiveDirectoryIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(activedirectoryidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), 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 *FakeActiveDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(activedirectoryidentityprovidersResource, "status", c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeActiveDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeActiveDirectoryIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(activedirectoryidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.ActiveDirectoryIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *FakeActiveDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(activedirectoryidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} 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 5e9278316..ff95d7831 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) ActiveDirectoryIdentityProviders(namespace string) v1alpha1.ActiveDirectoryIdentityProviderInterface { + return &FakeActiveDirectoryIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } 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 137892f3b..a7fdb511d 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,6 +5,8 @@ package v1alpha1 +type ActiveDirectoryIdentityProviderExpansion interface{} + 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 900f258a2..fa8653226 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 + ActiveDirectoryIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -22,6 +23,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface { + return newActiveDirectoryIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/1.20/client/supervisor/informers/externalversions/generic.go b/generated/1.20/client/supervisor/informers/externalversions/generic.go index b7821644d..f8460123b 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("activedirectoryidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..2d6202947 --- /dev/null +++ b/generated/1.20/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderInformer provides access to a shared informer and lister for +// ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ActiveDirectoryIdentityProviderLister +} + +type activeDirectoryIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewActiveDirectoryIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewFilteredActiveDirectoryIdentityProviderInformer(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().ActiveDirectoryIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().ActiveDirectoryIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.ActiveDirectoryIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *activeDirectoryIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *activeDirectoryIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.ActiveDirectoryIdentityProvider{}, f.defaultInformer) +} + +func (f *activeDirectoryIdentityProviderInformer) Lister() v1alpha1.ActiveDirectoryIdentityProviderLister { + return v1alpha1.NewActiveDirectoryIdentityProviderLister(f.Informer().GetIndexer()) +} 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 34f8361f5..5bd81fac2 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 { + // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. + ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -28,6 +30,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. +func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer { + return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/1.20/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/1.20/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..7008d59a7 --- /dev/null +++ b/generated/1.20/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderLister helps list ActiveDirectoryIdentityProviders. +// All objects returned here must be treated as read-only. +type ActiveDirectoryIdentityProviderLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister + ActiveDirectoryIdentityProviderListerExpansion +} + +// activeDirectoryIdentityProviderLister implements the ActiveDirectoryIdentityProviderLister interface. +type activeDirectoryIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewActiveDirectoryIdentityProviderLister returns a new ActiveDirectoryIdentityProviderLister. +func NewActiveDirectoryIdentityProviderLister(indexer cache.Indexer) ActiveDirectoryIdentityProviderLister { + return &activeDirectoryIdentityProviderLister{indexer: indexer} +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer. +func (s *activeDirectoryIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. +func (s *activeDirectoryIdentityProviderLister) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister { + return activeDirectoryIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ActiveDirectoryIdentityProviderNamespaceLister helps list and get ActiveDirectoryIdentityProviders. +// All objects returned here must be treated as read-only. +type ActiveDirectoryIdentityProviderNamespaceLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + ActiveDirectoryIdentityProviderNamespaceListerExpansion +} + +// activeDirectoryIdentityProviderNamespaceLister implements the ActiveDirectoryIdentityProviderNamespaceLister +// interface. +type activeDirectoryIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. +func (s activeDirectoryIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. +func (s activeDirectoryIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("activedirectoryidentityprovider"), name) + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), nil +} 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 28f41bd77..7c625e804 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 +// ActiveDirectoryIdentityProviderListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderLister. +type ActiveDirectoryIdentityProviderListerExpansion interface{} + +// ActiveDirectoryIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderNamespaceLister. +type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml new file mode 100644 index 000000000..03fbcd085 --- /dev/null +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -0,0 +1,271 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.0 + creationTimestamp: null + name: activedirectoryidentityproviders.idp.supervisor.pinniped.dev +spec: + group: idp.supervisor.pinniped.dev + names: + categories: + - pinniped + - pinniped-idp + - pinniped-idps + kind: ActiveDirectoryIdentityProvider + listKind: ActiveDirectoryIdentityProviderList + plural: activedirectoryidentityproviders + singular: activedirectoryidentityprovider + 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: ActiveDirectoryIdentityProvider describes the configuration of + an upstream Microsoft Active Directory 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: + bind: + description: Bind contains the configuration for how to provide access + credentials during an initial bind to the ActiveDirectory 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 + Active Directory 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 (distinguished name) 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: + - secretName + type: object + groupSearch: + description: GroupSearch contains the configuration for searching + for a user's group membership in ActiveDirectory. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each ActiveDirectory entry which was found + as the result of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the Active Directory entries whose value shall become + a group name in the user's list of groups after a successful + authentication. The value of this field is case-sensitive + and must match the case of the attribute name returned by + the ActiveDirectory server in the user's entry. E.g. "cn" + for common name. Distinguished names can be used by specifying + lower-case "dn". Optional. When not specified, the default + will act as if the GroupName were specified as "dn" (distinguished + name). + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". + When not specified, no group search will be performed and authenticated + users will not belong to any groups from the ActiveDirectory + provider. Also, when not specified, the values of Filter and + Attributes are ignored. + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for groups for a user. The + pattern "{}" must occur in the filter at least once and will + be dynamically replaced by the dn (distinguished name) of the + user entry found as a result of the user search. E.g. "member={}" + or "&(objectClass=groupOfNames)(member={})". For more information + about ActiveDirectory 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 "member={}". + type: string + type: object + host: + description: 'Host is the hostname of this Active Directory 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). + 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 Active Directory. + properties: + attributes: + description: Attributes specifies how the user's information should + be read from the ActiveDirectory entry which was found as the + result of the user search. + properties: + uid: + description: UID specifies the name of the attribute in the + ActiveDirectory entry which whose value shall be used to + uniquely identify the user within this ActiveDirectory provider + after a successful authentication. Optional, when empty + this defaults to "objectGUID". + type: string + username: + description: Username specifies the name of the attribute + in the ActiveDirectory entry whose value shall become the + username of the user after a successful authentication. + This would typically be the same attribute name used in + Optional, when empty this defaults to "sAMAccountName". + type: string + type: object + base: + description: Base is the dn (distinguished name) that should be + used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + Optional, when not specified it will be defaulted based on the + host, for example if your active directory host is "activedirectory.example.com:636", + it will be "dc=activedirectory,dc=example,dc=com". + type: string + filter: + description: Filter is the ActiveDirectory search filter which + should be applied when searching for users. The pattern "{}" + must occur in the filter at least once 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 ActiveDirectory 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. + type: string + type: object + required: + - host + type: object + 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 ActiveDirectoryIdentityProvider. + 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 ddc9c3607..e3406a814 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/register.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/register.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 v1alpha1 @@ -34,6 +34,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &OIDCIdentityProviderList{}, &LDAPIdentityProvider{}, &LDAPIdentityProviderList{}, + &ActiveDirectoryIdentityProvider{}, + &ActiveDirectoryIdentityProviderList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go new file mode 100644 index 000000000..18726e7bd --- /dev/null +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -0,0 +1,167 @@ +// 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 ActiveDirectoryIdentityProviderPhase string + +const ( + // ActiveDirectoryPhasePending is the default phase for newly-created ActiveDirectoryIdentityProvider resources. + ActiveDirectoryPhasePending ActiveDirectoryIdentityProviderPhase = "Pending" + + // ActiveDirectoryPhaseReady is the phase for an ActiveDirectoryIdentityProvider resource in a healthy state. + ActiveDirectoryPhaseReady ActiveDirectoryIdentityProviderPhase = "Ready" + + // ActiveDirectoryPhaseError is the phase for an ActiveDirectoryIdentityProvider in an unhealthy state. + ActiveDirectoryPhaseError ActiveDirectoryIdentityProviderPhase = "Error" +) + +// Status of an Active Directory identity provider. +type ActiveDirectoryIdentityProviderStatus struct { + // Phase summarizes the overall status of the ActiveDirectoryIdentityProvider. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase ActiveDirectoryIdentityProviderPhase `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 ActiveDirectoryIdentityProviderBind struct { + // SecretName contains the name of a namespace-local Secret object that provides the username and + // password for an Active Directory 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 (distinguished name) 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 ActiveDirectoryIdentityProviderUserSearchAttributes struct { + // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // of the user after a successful authentication. This would typically be the same attribute name used in + // Optional, when empty this defaults to "sAMAccountName". + // +optional + Username string `json:"username,omitempty"` + + // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely + // identify the user within this ActiveDirectory provider after a successful authentication. + // Optional, when empty this defaults to "objectGUID". + // +optional + UID string `json:"uid,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name + // in the user's list of groups after a successful authentication. + // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + +type ActiveDirectoryIdentityProviderUserSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for users. + // E.g. "ou=users,dc=example,dc=com". + // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is + // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // in the filter at least once 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 ActiveDirectory 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. + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as + // the result of the user search. + // +optional + Attributes ActiveDirectoryIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` +} + +type ActiveDirectoryIdentityProviderGroupSearch struct { + // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. + // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and + // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. + // The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the + // dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or + // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as + // the result of the group search. + // +optional + Attributes ActiveDirectoryIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + +// Spec for configuring an ActiveDirectory identity provider. +type ActiveDirectoryIdentityProviderSpec struct { + // Host is the hostname of this Active Directory 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 *TLSSpec `json:"tls,omitempty"` + + // Bind contains the configuration for how to provide access credentials during an initial bind to the ActiveDirectory server + // to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. + Bind ActiveDirectoryIdentityProviderBind `json:"bind,omitempty"` + + // UserSearch contains the configuration for searching for a user by name in Active Directory. + UserSearch ActiveDirectoryIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in ActiveDirectory. + GroupSearch ActiveDirectoryIdentityProviderGroupSearch `json:"groupSearch,omitempty"` +} + +// ActiveDirectoryIdentityProvider describes the configuration of an upstream Microsoft Active Directory 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 ActiveDirectoryIdentityProvider struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Spec for configuring the identity provider. + Spec ActiveDirectoryIdentityProviderSpec `json:"spec"` + + // Status of the identity provider. + Status ActiveDirectoryIdentityProviderStatus `json:"status,omitempty"` +} + +// List of ActiveDirectoryIdentityProvider objects. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type ActiveDirectoryIdentityProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ActiveDirectoryIdentityProvider `json:"items"` +} diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_meta.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_meta.go index e59976ff3..e04c6f2ae 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_meta.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_meta.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 v1alpha1 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 f7762b09b..9895a76e2 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -11,6 +11,196 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProvider) DeepCopyInto(out *ActiveDirectoryIdentityProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProvider. +func (in *ActiveDirectoryIdentityProvider) DeepCopy() *ActiveDirectoryIdentityProvider { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProvider) 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 *ActiveDirectoryIdentityProviderBind) DeepCopyInto(out *ActiveDirectoryIdentityProviderBind) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderBind. +func (in *ActiveDirectoryIdentityProviderBind) DeepCopy() *ActiveDirectoryIdentityProviderBind { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderBind) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearch. +func (in *ActiveDirectoryIdentityProviderGroupSearch) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderGroupSearchAttributes. +func (in *ActiveDirectoryIdentityProviderGroupSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderList) DeepCopyInto(out *ActiveDirectoryIdentityProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ActiveDirectoryIdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderList. +func (in *ActiveDirectoryIdentityProviderList) DeepCopy() *ActiveDirectoryIdentityProviderList { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ActiveDirectoryIdentityProviderList) 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 *ActiveDirectoryIdentityProviderSpec) DeepCopyInto(out *ActiveDirectoryIdentityProviderSpec) { + *out = *in + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSSpec) + **out = **in + } + out.Bind = in.Bind + out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderSpec. +func (in *ActiveDirectoryIdentityProviderSpec) DeepCopy() *ActiveDirectoryIdentityProviderSpec { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopyInto(out *ActiveDirectoryIdentityProviderStatus) { + *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 +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderStatus. +func (in *ActiveDirectoryIdentityProviderStatus) DeepCopy() *ActiveDirectoryIdentityProviderStatus { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearch. +func (in *ActiveDirectoryIdentityProviderUserSearch) DeepCopy() *ActiveDirectoryIdentityProviderUserSearch { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopyInto(out *ActiveDirectoryIdentityProviderUserSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveDirectoryIdentityProviderUserSearchAttributes. +func (in *ActiveDirectoryIdentityProviderUserSearchAttributes) DeepCopy() *ActiveDirectoryIdentityProviderUserSearchAttributes { + if in == nil { + return nil + } + out := new(ActiveDirectoryIdentityProviderUserSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Condition) DeepCopyInto(out *Condition) { *out = *in diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..40990cd98 --- /dev/null +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProvidersGetter has a method to return a ActiveDirectoryIdentityProviderInterface. +// A group's client should implement this interface. +type ActiveDirectoryIdentityProvidersGetter interface { + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface +} + +// ActiveDirectoryIdentityProviderInterface has methods to work with ActiveDirectoryIdentityProvider resources. +type ActiveDirectoryIdentityProviderInterface interface { + Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, 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.ActiveDirectoryIdentityProvider, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.ActiveDirectoryIdentityProviderList, 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.ActiveDirectoryIdentityProvider, err error) + ActiveDirectoryIdentityProviderExpansion +} + +// activeDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type activeDirectoryIdentityProviders struct { + client rest.Interface + ns string +} + +// newActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviders +func newActiveDirectoryIdentityProviders(c *IDPV1alpha1Client, namespace string) *activeDirectoryIdentityProviders { + return &activeDirectoryIdentityProviders{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *activeDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *activeDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha1.ActiveDirectoryIdentityProviderList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested activeDirectoryIdentityProviders. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Post(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *activeDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + 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 *activeDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Put(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(activeDirectoryIdentityProvider.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(activeDirectoryIdentityProvider). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *activeDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *activeDirectoryIdentityProviders) 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("activedirectoryidentityproviders"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *activeDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + result = &v1alpha1.ActiveDirectoryIdentityProvider{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("activedirectoryidentityproviders"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.go new file mode 100644 index 000000000..d8e88b4d7 --- /dev/null +++ b/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1/fake/fake_activedirectoryidentityprovider.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" +) + +// FakeActiveDirectoryIdentityProviders implements ActiveDirectoryIdentityProviderInterface +type FakeActiveDirectoryIdentityProviders struct { + Fake *FakeIDPV1alpha1 + ns string +} + +var activedirectoryidentityprovidersResource = schema.GroupVersionResource{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Resource: "activedirectoryidentityproviders"} + +var activedirectoryidentityprovidersKind = schema.GroupVersionKind{Group: "idp.supervisor.pinniped.dev", Version: "v1alpha1", Kind: "ActiveDirectoryIdentityProvider"} + +// Get takes name of the activeDirectoryIdentityProvider, and returns the corresponding activeDirectoryIdentityProvider object, and an error if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// List takes label and field selectors, and returns the list of ActiveDirectoryIdentityProviders that match those selectors. +func (c *FakeActiveDirectoryIdentityProviders) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.ActiveDirectoryIdentityProviderList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(activedirectoryidentityprovidersResource, activedirectoryidentityprovidersKind, c.ns, opts), &v1alpha1.ActiveDirectoryIdentityProviderList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.ActiveDirectoryIdentityProviderList{ListMeta: obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).ListMeta} + for _, item := range obj.(*v1alpha1.ActiveDirectoryIdentityProviderList).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 activeDirectoryIdentityProviders. +func (c *FakeActiveDirectoryIdentityProviders) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(activedirectoryidentityprovidersResource, c.ns, opts)) + +} + +// Create takes the representation of a activeDirectoryIdentityProvider and creates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Create(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.CreateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Update takes the representation of a activeDirectoryIdentityProvider and updates it. Returns the server's representation of the activeDirectoryIdentityProvider, and an error, if there is any. +func (c *FakeActiveDirectoryIdentityProviders) Update(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(activedirectoryidentityprovidersResource, c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), 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 *FakeActiveDirectoryIdentityProviders) UpdateStatus(ctx context.Context, activeDirectoryIdentityProvider *v1alpha1.ActiveDirectoryIdentityProvider, opts v1.UpdateOptions) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(activedirectoryidentityprovidersResource, "status", c.ns, activeDirectoryIdentityProvider), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} + +// Delete takes name of the activeDirectoryIdentityProvider and deletes it. Returns an error if one occurs. +func (c *FakeActiveDirectoryIdentityProviders) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(activedirectoryidentityprovidersResource, c.ns, name), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeActiveDirectoryIdentityProviders) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(activedirectoryidentityprovidersResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha1.ActiveDirectoryIdentityProviderList{}) + return err +} + +// Patch applies the patch and returns the patched activeDirectoryIdentityProvider. +func (c *FakeActiveDirectoryIdentityProviders) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.ActiveDirectoryIdentityProvider, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(activedirectoryidentityprovidersResource, c.ns, name, pt, data, subresources...), &v1alpha1.ActiveDirectoryIdentityProvider{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), err +} 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 c06f74294..c089ba8b7 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) ActiveDirectoryIdentityProviders(namespace string) v1alpha1.ActiveDirectoryIdentityProviderInterface { + return &FakeActiveDirectoryIdentityProviders{c, namespace} +} + func (c *FakeIDPV1alpha1) LDAPIdentityProviders(namespace string) v1alpha1.LDAPIdentityProviderInterface { return &FakeLDAPIdentityProviders{c, namespace} } 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 137892f3b..a7fdb511d 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,6 +5,8 @@ package v1alpha1 +type ActiveDirectoryIdentityProviderExpansion interface{} + 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 a32a2dd1b..f492b7e61 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 + ActiveDirectoryIdentityProvidersGetter LDAPIdentityProvidersGetter OIDCIdentityProvidersGetter } @@ -22,6 +23,10 @@ type IDPV1alpha1Client struct { restClient rest.Interface } +func (c *IDPV1alpha1Client) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderInterface { + return newActiveDirectoryIdentityProviders(c, namespace) +} + func (c *IDPV1alpha1Client) LDAPIdentityProviders(namespace string) LDAPIdentityProviderInterface { return newLDAPIdentityProviders(c, namespace) } diff --git a/generated/latest/client/supervisor/informers/externalversions/generic.go b/generated/latest/client/supervisor/informers/externalversions/generic.go index 338a4d721..6c6b427d4 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("activedirectoryidentityproviders"): + return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().ActiveDirectoryIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("ldapidentityproviders"): return &genericInformer{resource: resource.GroupResource(), informer: f.IDP().V1alpha1().LDAPIdentityProviders().Informer()}, nil case idpv1alpha1.SchemeGroupVersion.WithResource("oidcidentityproviders"): diff --git a/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..d0393f4eb --- /dev/null +++ b/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderInformer provides access to a shared informer and lister for +// ActiveDirectoryIdentityProviders. +type ActiveDirectoryIdentityProviderInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.ActiveDirectoryIdentityProviderLister +} + +type activeDirectoryIdentityProviderInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewActiveDirectoryIdentityProviderInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredActiveDirectoryIdentityProviderInformer constructs a new informer for ActiveDirectoryIdentityProvider 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 NewFilteredActiveDirectoryIdentityProviderInformer(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().ActiveDirectoryIdentityProviders(namespace).List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.IDPV1alpha1().ActiveDirectoryIdentityProviders(namespace).Watch(context.TODO(), options) + }, + }, + &idpv1alpha1.ActiveDirectoryIdentityProvider{}, + resyncPeriod, + indexers, + ) +} + +func (f *activeDirectoryIdentityProviderInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredActiveDirectoryIdentityProviderInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *activeDirectoryIdentityProviderInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&idpv1alpha1.ActiveDirectoryIdentityProvider{}, f.defaultInformer) +} + +func (f *activeDirectoryIdentityProviderInformer) Lister() v1alpha1.ActiveDirectoryIdentityProviderLister { + return v1alpha1.NewActiveDirectoryIdentityProviderLister(f.Informer().GetIndexer()) +} 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 1a1c2d570..4f19f3367 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 { + // ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. + ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. LDAPIdentityProviders() LDAPIdentityProviderInformer // OIDCIdentityProviders returns a OIDCIdentityProviderInformer. @@ -28,6 +30,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// ActiveDirectoryIdentityProviders returns a ActiveDirectoryIdentityProviderInformer. +func (v *version) ActiveDirectoryIdentityProviders() ActiveDirectoryIdentityProviderInformer { + return &activeDirectoryIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // LDAPIdentityProviders returns a LDAPIdentityProviderInformer. func (v *version) LDAPIdentityProviders() LDAPIdentityProviderInformer { return &lDAPIdentityProviderInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/generated/latest/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go b/generated/latest/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.go new file mode 100644 index 000000000..47c573cc4 --- /dev/null +++ b/generated/latest/client/supervisor/listers/idp/v1alpha1/activedirectoryidentityprovider.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" +) + +// ActiveDirectoryIdentityProviderLister helps list ActiveDirectoryIdentityProviders. +// All objects returned here must be treated as read-only. +type ActiveDirectoryIdentityProviderLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. + ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister + ActiveDirectoryIdentityProviderListerExpansion +} + +// activeDirectoryIdentityProviderLister implements the ActiveDirectoryIdentityProviderLister interface. +type activeDirectoryIdentityProviderLister struct { + indexer cache.Indexer +} + +// NewActiveDirectoryIdentityProviderLister returns a new ActiveDirectoryIdentityProviderLister. +func NewActiveDirectoryIdentityProviderLister(indexer cache.Indexer) ActiveDirectoryIdentityProviderLister { + return &activeDirectoryIdentityProviderLister{indexer: indexer} +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer. +func (s *activeDirectoryIdentityProviderLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// ActiveDirectoryIdentityProviders returns an object that can list and get ActiveDirectoryIdentityProviders. +func (s *activeDirectoryIdentityProviderLister) ActiveDirectoryIdentityProviders(namespace string) ActiveDirectoryIdentityProviderNamespaceLister { + return activeDirectoryIdentityProviderNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ActiveDirectoryIdentityProviderNamespaceLister helps list and get ActiveDirectoryIdentityProviders. +// All objects returned here must be treated as read-only. +type ActiveDirectoryIdentityProviderNamespaceLister interface { + // List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) + // Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) + ActiveDirectoryIdentityProviderNamespaceListerExpansion +} + +// activeDirectoryIdentityProviderNamespaceLister implements the ActiveDirectoryIdentityProviderNamespaceLister +// interface. +type activeDirectoryIdentityProviderNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ActiveDirectoryIdentityProviders in the indexer for a given namespace. +func (s activeDirectoryIdentityProviderNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.ActiveDirectoryIdentityProvider, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.ActiveDirectoryIdentityProvider)) + }) + return ret, err +} + +// Get retrieves the ActiveDirectoryIdentityProvider from the indexer for a given namespace and name. +func (s activeDirectoryIdentityProviderNamespaceLister) Get(name string) (*v1alpha1.ActiveDirectoryIdentityProvider, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("activedirectoryidentityprovider"), name) + } + return obj.(*v1alpha1.ActiveDirectoryIdentityProvider), nil +} 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 28f41bd77..7c625e804 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 +// ActiveDirectoryIdentityProviderListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderLister. +type ActiveDirectoryIdentityProviderListerExpansion interface{} + +// ActiveDirectoryIdentityProviderNamespaceListerExpansion allows custom methods to be added to +// ActiveDirectoryIdentityProviderNamespaceLister. +type ActiveDirectoryIdentityProviderNamespaceListerExpansion interface{} + // LDAPIdentityProviderListerExpansion allows custom methods to be added to // LDAPIdentityProviderLister. type LDAPIdentityProviderListerExpansion interface{} diff --git a/go.mod b/go.mod index 584c25a58..7c91fc2ea 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/go-logr/stdr v0.4.0 github.com/go-openapi/spec v0.20.3 // indirect github.com/gofrs/flock v0.8.1 + github.com/gofrs/uuid v3.2.0+incompatible github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.6 github.com/google/gofuzz v1.2.0 diff --git a/go.sum b/go.sum index 6c545de39..fce7b4427 100644 --- a/go.sum +++ b/go.sum @@ -554,6 +554,7 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= diff --git a/internal/upstreamad/upstreamad.go b/internal/upstreamad/upstreamad.go new file mode 100644 index 000000000..eff0a9f90 --- /dev/null +++ b/internal/upstreamad/upstreamad.go @@ -0,0 +1,654 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package upstreamad implements an active directory specific abstraction of upstream LDAP IDP interactions. +package upstreamad + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "net" + "net/url" + "sort" + "strings" + "time" + + "github.com/go-ldap/ldap/v3" + "github.com/gofrs/uuid" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/utils/trace" + + "go.pinniped.dev/internal/authenticators" + "go.pinniped.dev/internal/endpointaddr" + "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/plog" +) + +const ( + ldapsScheme = "ldaps" + distinguishedNameAttributeName = "dn" + objectGUIDAttributeName = "objectGUID" + sAMAccountNameAttributeName = "sAMAccountName" + searchFilterInterpolationLocationMarker = "{}" + groupSearchPageSize = uint32(250) + defaultLDAPPort = uint16(389) + defaultLDAPSPort = uint16(636) +) + +// Conn abstracts the upstream LDAP communication protocol (mostly for testing). +type Conn interface { + Bind(username, password string) error + + Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) + + SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) + + Close() +} + +// Our Conn type is subset of the ldap.Client interface, which is implemented by ldap.Conn. +var _ Conn = &ldap.Conn{} + +// 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, addr endpointaddr.HostPort) (Conn, error) +} + +// LDAPDialerFunc makes it easy to use a func as an LDAPDialer. +type LDAPDialerFunc func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) + +var _ LDAPDialer = LDAPDialerFunc(nil) + +func (f LDAPDialerFunc) Dial(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) { + return f(ctx, addr) +} + +type LDAPConnectionProtocol string + +const ( + StartTLS = LDAPConnectionProtocol("StartTLS") + TLS = LDAPConnectionProtocol("TLS") +) + +// 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. +// 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 + + // 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 + + // ConnectionProtocol determines how to establish the connection to the server. Either StartTLS or TLS. + ConnectionProtocol LDAPConnectionProtocol + + // 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 active directory IDP. + BindUsername string + + // BindPassword is the password to use when performing a bind with the upstream active directory IDP. + BindPassword string + + // UserSearch contains information about how to search for users in the upstream active directory IDP. + UserSearch UserSearchConfig + + // GroupSearch contains information about how to search for group membership in the upstream active directory IDP. + GroupSearch GroupSearchConfig + + // Dialer exists to enable testing. When nil, will use a default appropriate for production use. + Dialer LDAPDialer +} + +// UserSearchConfig contains information about how to search for users in the upstream active directory IDP. +type UserSearchConfig struct { + // Base is the base DN to use for the user search in the upstream active directory IDP. + Base string + + // Filter is the filter to use for the user search in the upstream active directory IDP. + Filter string + + // UsernameAttribute is the attribute in the LDAP entry from which the username should be + // retrieved. Empty means to use 'sAMAccountName'. + UsernameAttribute string + + // UIDAttribute is the attribute in the LDAP entry from which the user's unique ID should be + // retrieved. Empty means to use 'objectGUID'. + UIDAttribute string +} + +// GroupSearchConfig contains information about how to search for group membership for users in the upstream active directory IDP. +type GroupSearchConfig struct { + // Base is the base DN to use for the group search in the upstream active directory IDP. Empty means to skip group search + // entirely, in which case authenticated users will not belong to any groups from the upstream active directory IDP. + Base string + + // Filter is the filter to use for the group search in the upstream active directory IDP. Empty means to use `member={}`. + Filter string + + // GroupNameAttribute is the attribute in the LDAP group entry from which the group name should be + // retrieved. Empty means to use 'cn'. + GroupNameAttribute string +} + +type Provider struct { + c ProviderConfig +} + +var _ provider.UpstreamLDAPIdentityProviderI = &Provider{} +var _ authenticators.UserAuthenticator = &Provider{} + +// 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) { + tlsAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPSPort) + if err != nil { + return nil, ldap.NewError(ldap.ErrorNetwork, err) + } + + startTLSAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPPort) + if err != nil { + return nil, ldap.NewError(ldap.ErrorNetwork, err) + } + + // Choose how and where to dial based on TLS vs. StartTLS config option. + var dialFunc LDAPDialerFunc + var addr endpointaddr.HostPort + switch { + case p.c.ConnectionProtocol == TLS: + dialFunc = p.dialTLS + addr = tlsAddr + case p.c.ConnectionProtocol == StartTLS: + dialFunc = p.dialStartTLS + addr = startTLSAddr + default: + return nil, ldap.NewError(ldap.ErrorNetwork, fmt.Errorf("did not specify valid ConnectionProtocol")) + } + + // Override the real dialer for testing purposes sometimes. + if p.c.Dialer != nil { + dialFunc = p.c.Dialer.Dial + } + + return dialFunc(ctx, addr) +} + +// dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is TLS. +// 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, addr endpointaddr.HostPort) (Conn, error) { + tlsConfig, err := p.tlsConfig() + if err != nil { + return nil, ldap.NewError(ldap.ErrorNetwork, err) + } + + dialer := &tls.Dialer{NetDialer: netDialer(), Config: tlsConfig} + c, err := dialer.DialContext(ctx, "tcp", addr.Endpoint()) + if err != nil { + return nil, ldap.NewError(ldap.ErrorNetwork, err) + } + + conn := ldap.NewConn(c, true) + conn.Start() + return conn, nil +} + +// dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is StartTLS. +// 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) dialStartTLS(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) { + tlsConfig, err := p.tlsConfig() + if err != nil { + return nil, ldap.NewError(ldap.ErrorNetwork, err) + } + + // Unfortunately, this seems to be required for StartTLS, even though it is not needed for regular TLS. + tlsConfig.ServerName = addr.Host + + c, err := netDialer().DialContext(ctx, "tcp", addr.Endpoint()) + if err != nil { + return nil, ldap.NewError(ldap.ErrorNetwork, err) + } + + conn := ldap.NewConn(c, false) + conn.Start() + err = conn.StartTLS(tlsConfig) + if err != nil { + return nil, err + } + + return conn, nil +} + +func netDialer() *net.Dialer { + return &net.Dialer{Timeout: time.Minute} +} + +func (p *Provider) tlsConfig() (*tls.Config, error) { + var rootCAs *x509.CertPool + if p.c.CABundle != nil { + rootCAs = x509.NewCertPool() + if !rootCAs.AppendCertsFromPEM(p.c.CABundle) { + return nil, fmt.Errorf("could not parse CA bundle") + } + } + return &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: rootCAs}, nil +} + +// A name for this upstream provider. +func (p *Provider) GetName() string { + return p.c.Name +} + +// Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base". +// 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() *url.URL { + u := &url.URL{Scheme: ldapsScheme, Host: p.c.Host} + q := u.Query() + q.Set("base", p.c.UserSearch.Base) + u.RawQuery = q.Encode() + return u +} + +// 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) 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 +} + +// 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) 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 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) { + 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, mappedGroupNames, 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 + } + + response := &authenticator.Response{ + User: &user.DefaultInfo{ + Name: mappedUsername, + UID: mappedUID, + Groups: mappedGroupNames, + }, + } + p.traceAuthSuccess(t) + return response, true, nil +} + +func (p *Provider) searchGroupsForUserDN(conn Conn, userDN string) ([]string, error) { + searchResult, err := conn.SearchWithPaging(p.groupSearchRequest(userDN), groupSearchPageSize) + if err != nil { + return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err) + } + + groupAttributeName := p.c.GroupSearch.GroupNameAttribute + if len(groupAttributeName) == 0 { + groupAttributeName = distinguishedNameAttributeName + } + + groups := []string{} + for _, groupEntry := range searchResult.Entries { + if len(groupEntry.DN) == 0 { + return nil, fmt.Errorf(`searching for group memberships for user with DN %q resulted in search result without DN`, userDN) + } + mappedGroupName, err := p.getSearchResultAttributeValue(groupAttributeName, groupEntry, userDN) + if err != nil { + return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err) + } + groups = append(groups, mappedGroupName) + } + + return groups, nil +} + +func (p *Provider) validateConfig() error { + // TODO if user search base is nil then host must be an IP address? + if p.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, bindFunc func(conn Conn, foundUserDN string) error) (string, string, []string, error) { + searchResult, err := conn.Search(p.userSearchRequest(username)) + if err != nil { + plog.All(`error searching for user`, + "upstreamName", p.GetName(), + "username", username, + "err", err, + ) + return "", "", nil, fmt.Errorf(`error searching for user: %w`, err) + } + if len(searchResult.Entries) == 0 { + if plog.Enabled(plog.LevelAll) { + plog.All("error finding user: user not found (if this username is valid, please check the user search configuration)", + "upstreamName", p.GetName(), + "username", username, + ) + } else { + plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName()) + } + return "", "", nil, nil + } + + // At this point, we have matched at least one entry, so we can be confident that the username is not actually + // someone's password mistakenly entered into the username field, so we can log it without concern. + if len(searchResult.Entries) > 1 { + return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`, + username, len(searchResult.Entries), + ) + } + userEntry := searchResult.Entries[0] + if len(userEntry.DN) == 0 { + return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username) + } + + mappedUsername, err := p.getSearchResultAttributeValue(p.usernameAttribute(), userEntry, username) + if err != nil { + return "", "", nil, err + } + + // We would like to support binary typed attributes for UIDs, so always read them as binary and encode them, + // even when the attribute may not be binary. + mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.uidAttribute(), userEntry, username) + if err != nil { + return "", "", nil, err + } + + mappedGroupNames := []string{} + if len(p.c.GroupSearch.Base) > 0 { + mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN) + if err != nil { + return "", "", nil, err + } + } + sort.Strings(mappedGroupNames) + + // Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername! + err = bindFunc(conn, userEntry.DN) + 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) + ldapErr := &ldap.Error{} + if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials { + return "", "", nil, nil + } + return "", "", nil, fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) + } + + return mappedUsername, mappedUID, mappedGroupNames, nil +} + +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.userSearchBase(), + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.NeverDerefAliases, + 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) userSearchBase() string { + if len(p.c.UserSearch.Base) == 0 { + parsed, err := endpointaddr.Parse(p.c.Host, 636) + if err != nil { + return "" + } + dcParts := strings.Split(parsed.Host, ".") + base := "" + for i, dcPart := range dcParts { + base += "dc=" + dcPart + if i < len(dcParts)-1 { + base += "," + } + } + return base + } + return p.c.UserSearch.Base +} + +func (p *Provider) groupSearchRequest(userDN string) *ldap.SearchRequest { + // See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options. + return &ldap.SearchRequest{ + BaseDN: p.c.GroupSearch.Base, + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.NeverDerefAliases, + SizeLimit: 0, // unlimited size because we will search with paging + TimeLimit: 90, + TypesOnly: false, + Filter: p.groupSearchFilter(userDN), + Attributes: p.groupSearchRequestedAttributes(), + Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us + } +} + +func (p *Provider) userSearchRequestedAttributes() []string { + attributes := []string{} + if p.usernameAttribute() != distinguishedNameAttributeName { + attributes = append(attributes, p.usernameAttribute()) + } + if p.uidAttribute() != distinguishedNameAttributeName { + attributes = append(attributes, p.uidAttribute()) + } + return attributes +} + +func (p *Provider) groupSearchRequestedAttributes() []string { + switch p.c.GroupSearch.GroupNameAttribute { + case "": + return []string{} + case distinguishedNameAttributeName: + return []string{} + default: + return []string{p.c.GroupSearch.GroupNameAttribute} + } +} + +func (p *Provider) usernameAttribute() string { + if len(p.c.UserSearch.UsernameAttribute) == 0 { + return sAMAccountNameAttributeName + } + return p.c.UserSearch.UsernameAttribute +} + +func (p *Provider) uidAttribute() string { + if len(p.c.UserSearch.UIDAttribute) == 0 { + return objectGUIDAttributeName + } + return p.c.UserSearch.UIDAttribute +} + +func (p *Provider) userSearchFilter(username string) string { + safeUsername := p.escapeUsernameForSearchFilter(username) + if len(p.c.UserSearch.Filter) == 0 { + return fmt.Sprintf("(%s=%s)", p.usernameAttribute(), safeUsername) + } + return interpolateSearchFilter(p.c.UserSearch.Filter, safeUsername) +} + +func (p *Provider) groupSearchFilter(userDN string) string { + if len(p.c.GroupSearch.Filter) == 0 { + return fmt.Sprintf("(member=%s)", userDN) + } + return interpolateSearchFilter(p.c.GroupSearch.Filter, userDN) +} + +func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string) string { + filter := strings.ReplaceAll(filterFormat, searchFilterInterpolationLocationMarker, valueToInterpolateIntoFilter) + if strings.HasPrefix(filter, "(") && strings.HasSuffix(filter, ")") { + return filter + } + return "(" + filter + ")" +} + +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) +} + +// Returns the (potentially) binary data of the attribute's value, base64 URL encoded. +func (p *Provider) getSearchResultAttributeRawValueEncoded(attributeName string, entry *ldap.Entry, username string) (string, error) { + if attributeName == distinguishedNameAttributeName { + return base64.RawURLEncoding.EncodeToString([]byte(entry.DN)), nil + } + + attributeValues := entry.GetRawAttributeValues(attributeName) + + if len(attributeValues) != 1 { + 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 { + return "", fmt.Errorf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, + attributeName, username, + ) + } + + if attributeName == objectGUIDAttributeName { + // In AD, objectGUID will be represented as a base64-encoded UUID. Convert it back to UUID encoding. + base64decoded, err := base64.StdEncoding.DecodeString(entry.GetAttributeValue(attributeName)) + if err != nil { + // TODO if there is an error, should we throw it or pass it through as base64? + return "", fmt.Errorf("Error decoding UID: %s", err.Error()) + } + uuidEntry, err := uuid.FromBytes(base64decoded) + if err != nil { + return "", fmt.Errorf("Error decoding UID: %s", err.Error()) + } + return uuidEntry.String(), nil + } + + return base64.RawURLEncoding.EncodeToString(attributeValue), nil +} + +func (p *Provider) getSearchResultAttributeValue(attributeName string, entry *ldap.Entry, username string) (string, error) { + if attributeName == distinguishedNameAttributeName { + return entry.DN, nil + } + + attributeValues := entry.GetAttributeValues(attributeName) + + if len(attributeValues) != 1 { + 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 { + 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 +} + +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}, + ) +} diff --git a/internal/upstreamad/upstreamad_test.go b/internal/upstreamad/upstreamad_test.go new file mode 100644 index 000000000..a518bae21 --- /dev/null +++ b/internal/upstreamad/upstreamad_test.go @@ -0,0 +1,1375 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package upstreamad + +import ( + "context" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "testing" + "time" + + "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/certauthority" + "go.pinniped.dev/internal/endpointaddr" + "go.pinniped.dev/internal/mocks/mockldapconn" + "go.pinniped.dev/internal/testutil" +) + +const ( + testHost = "activedirectory.example.com:8443" + testBindUsername = "cn=some-bind-username,dc=pinniped,dc=dev" + testBindPassword = "some-bind-password" + testUpstreamUsername = "some-upstream-username" + testUpstreamPassword = "some-upstream-password" + testUserSearchBase = "some-upstream-user-base-dn" + testGroupSearchBase = "some-upstream-group-base-dn" + testUserSearchFilter = "some-user-filter={}-and-more-filter={}" + testGroupSearchFilter = "some-group-filter={}-and-more-filter={}" + testUserSearchUsernameAttribute = "some-upstream-username-attribute" + testUserSearchUIDAttribute = "objectGUID" + testGroupSearchGroupNameAttribute = "some-upstream-group-name-attribute" + testUserSearchResultDNValue = "some-upstream-user-dn" + testGroupSearchResultDNValue1 = "some-upstream-group-dn1" + testGroupSearchResultDNValue2 = "some-upstream-group-dn2" + testUserSearchResultUsernameAttributeValue = "some-upstream-username-value" + testUserSearchResultUIDAttributeValue = "Ej5FZ+ibEtOkVkJmFBdAAA==" // this is base64 encoded 123e4567-e89b-12d3-a456-426614174000 + testGroupSearchResultGroupNameAttributeValue1 = "some-upstream-group-name-value1" + testGroupSearchResultGroupNameAttributeValue2 = "some-upstream-group-name-value2" + + expectedGroupSearchPageSize = uint32(250) +) + +var ( + testUserSearchFilterInterpolated = fmt.Sprintf("(some-user-filter=%s-and-more-filter=%s)", testUpstreamUsername, testUpstreamUsername) + testGroupSearchFilterInterpolated = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", testUserSearchResultDNValue, testUserSearchResultDNValue) +) + +func TestEndUserAuthentication(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 + ConnectionProtocol: TLS, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUserSearchUsernameAttribute, + UIDAttribute: testUserSearchUIDAttribute, + }, + GroupSearch: GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupSearchGroupNameAttribute, + }, + } + if editFunc != nil { + editFunc(config) + } + return config + } + + expectedUserSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { + request := &ldap.SearchRequest{ + BaseDN: testUserSearchBase, + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.NeverDerefAliases, + SizeLimit: 2, + TimeLimit: 90, + TypesOnly: false, + Filter: testUserSearchFilterInterpolated, + Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute}, + Controls: nil, // don't need paging because we set the SizeLimit so small + } + if editFunc != nil { + editFunc(request) + } + return request + } + + expectedGroupSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { + request := &ldap.SearchRequest{ + BaseDN: testGroupSearchBase, + Scope: ldap.ScopeWholeSubtree, + DerefAliases: ldap.NeverDerefAliases, + SizeLimit: 0, // unlimited size because we will search with paging + TimeLimit: 90, + TypesOnly: false, + Filter: testGroupSearchFilterInterpolated, + Attributes: []string{testGroupSearchGroupNameAttribute}, + Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us + } + if editFunc != nil { + editFunc(request) + } + return request + } + + exampleUserSearchResult := &ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + } + + exampleGroupSearchResult := &ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), + }, + }, + { + DN: testGroupSearchResultDNValue2, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue2}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + } + + // The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult. + expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticator.Response { + u := &user.DefaultInfo{ + Name: testUserSearchResultUsernameAttributeValue, + UID: "123e4567-e89b-12d3-a456-426614174000", + Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, + } + if editFunc != nil { + editFunc(u) + } + return &authenticator.Response{User: u} + } + + tests := []struct { + 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), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "default as much as possible", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: &ProviderConfig{ + Name: "some-provider-name", + Host: testHost, + CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test + ConnectionProtocol: TLS, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + GroupSearch: GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupSearchGroupNameAttribute, + }, + }, + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Filter = "(" + sAMAccountNameAttributeName + "=" + testUpstreamUsername + ")" + r.Attributes = []string{sAMAccountNameAttributeName, testUserSearchUIDAttribute} + r.BaseDN = "dc=activedirectory,dc=example,dc=com" + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(sAMAccountNameAttributeName, []string{testUserSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "when the user search filter is already wrapped by parenthesis then it is not wrapped again", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.UserSearch.Filter = "(" + testUserSearchFilter + ")" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "when the group search filter is already wrapped by parenthesis then it is not wrapped again", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.Filter = "(" + testGroupSearchFilter + ")" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "when the group search base is empty then skip the group search entirely", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.Base = "" // this configuration means that the user does not want group search to happen + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Groups = []string{} + }), + }, + { + name: "when the UsernameAttribute is dn and there is a user search filter provided", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.UserSearch.UsernameAttribute = "dn" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{testUserSearchUIDAttribute} + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Name = testUserSearchResultDNValue + }), + }, + { + name: "when the UIDAttribute is dn", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.UserSearch.UIDAttribute = "dn" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{testUserSearchUsernameAttribute} + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.UID = base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultDNValue)) + }), + }, + { + name: "when the GroupNameAttribute is empty then it defaults to dn", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "" // blank means to use dn + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{} + }), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2} + }), + }, + { + name: "when the GroupNameAttribute is dn", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "dn" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{} + }), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2} + }), + }, + { + name: "when the GroupNameAttribute is cn", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "cn" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{"cn"} + }), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("cn", []string{testGroupSearchResultGroupNameAttributeValue1}), + }, + }, + { + DN: testGroupSearchResultDNValue2, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("cn", []string{testGroupSearchResultGroupNameAttributeValue2}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "when user search Filter is blank it derives a search filter from the UsernameAttribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.UserSearch.Filter = "" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Filter = "(" + testUserSearchUsernameAttribute + "=" + testUpstreamUsername + ")" + })).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "when user search Filter and user attribute is blank it defaults to sAMAccountName={}", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.UserSearch.Filter = "" + p.UserSearch.UsernameAttribute = "" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Filter = "(" + sAMAccountNameAttributeName + "=" + testUpstreamUsername + ")" + r.Attributes = []string{sAMAccountNameAttributeName, testUserSearchUIDAttribute} + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(sAMAccountNameAttributeName, []string{testUserSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "when group search Filter is blank it uses a default search filter of member={}", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.Filter = "" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Filter = "(member=" + testUserSearchResultDNValue + ")" + }), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "when the username has special LDAP search filter characters then they must be properly escaped in the search filter, because the username is end-user input", + username: `a&b|c(d)e\f*g`, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Filter = fmt.Sprintf("(some-user-filter=%s-and-more-filter=%s)", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) + })).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, + { + name: "group names are sorted to make the result more stable/predictable", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{"c"}), + }, + }, + { + DN: testGroupSearchResultDNValue2, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{"a"}), + }, + }, + { + DN: testGroupSearchResultDNValue2, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{"b"}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: testUserSearchResultUsernameAttributeValue, + UID: "123e4567-e89b-12d3-a456-426614174000", + Groups: []string{"a", "b", "c"}, + }, + }, + }, + { + 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, + providerConfig: providerConfig(func(p *ProviderConfig) { + 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", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: 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, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(nil, errors.New("some user search error")).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: `error searching for user: some user search error`, + }, + { + name: "when searching for the user's groups returns an error", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(nil, errors.New("some group search error")).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf(`error searching for group memberships for user with DN "%s": some group search error`, testUserSearchResultDNValue), + }, + { + name: "when searching for the user returns no results", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantUnauthenticated: true, + }, + { + name: "when searching for the user returns multiple results", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + {DN: testUserSearchResultDNValue}, + {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, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(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's groups returns a group without a DN", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), + }, + }, + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue2}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf( + `searching for group memberships for user with DN "%s" resulted in search result without DN`, + testUserSearchResultDNValue), + }, + { + name: "when searching for the user returns a user without an expected username attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + }, 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 group memberships returns a group without an expected group name attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), + }, + }, + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("unrelated attribute", []string{"anything"}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf( + `error searching for group memberships for user with DN "%s": found 0 values for attribute "%s" while searching for user "%s", but expected 1 result`, + testUserSearchResultDNValue, testGroupSearchGroupNameAttribute, testUserSearchResultDNValue), + }, + { + 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), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{ + testUserSearchResultUsernameAttributeValue, + "unexpected-additional-value", + }), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + }, 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 group memberships returns a group with too many values for the expected group name attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), + }, + }, + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{ + testGroupSearchResultGroupNameAttributeValue1, + "unexpected-additional-value", + }), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf( + `error searching for group memberships for user with DN "%s": found 2 values for attribute "%s" while searching for user "%s", but expected 1 result`, + testUserSearchResultDNValue, testGroupSearchGroupNameAttribute, testUserSearchResultDNValue), + }, + { + 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), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{""}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), + }, + }, + }, + }, 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 group memberships returns a group with an empty value for for the expected group name attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), + }, + }, + { + DN: testGroupSearchResultDNValue1, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{""}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: fmt.Sprintf( + `error searching for group memberships for user with DN "%s": found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, + testUserSearchResultDNValue, testGroupSearchGroupNameAttribute, testUserSearchResultDNValue), + }, + { + name: "when searching for the user returns a user without an expected UID attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + }, + }, + }, + }, 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, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{ + testUserSearchResultUIDAttributeValue, + "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, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + 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, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, 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, testUserSearchResultDNValue), + }, + { + name: "when binding as the found user returns a specific invalid credentials error", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantUnauthenticated: true, + skipDryRunAuthenticateUser: true, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + err := &ldap.Error{ + Err: errors.New("some bind error"), + ResultCode: ldap.LDAPResultInvalidCredentials, + } + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Return(err).Times(1) + }, + }, + { + name: "when no username is specified", + username: "", + password: testUpstreamPassword, + providerConfig: providerConfig(nil), + wantToSkipDial: true, + wantUnauthenticated: true, + }, + } + + 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.searchMocks != nil { + tt.searchMocks(conn) + } + if tt.bindEndUserMocks != nil { + tt.bindEndUserMocks(conn) + } + + dialWasAttempted := false + tt.providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) { + dialWasAttempted = true + require.Equal(t, tt.providerConfig.Host, addr.Endpoint()) + if tt.dialError != nil { + return nil, tt.dialError + } + return conn, nil + }) + + 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) + 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) + } + }) + } +} + +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 + ConnectionProtocol: TLS, + 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, addr endpointaddr.HostPort) (Conn, error) { + dialWasAttempted = true + require.Equal(t, tt.providerConfig.Host, addr.Endpoint()) + 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", + 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?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev", + New(ProviderConfig{ + Host: "ldap.example.com:1234", + UserSearch: UserSearchConfig{Base: "ou=users,dc=pinniped,dc=dev"}, + }).GetURL().String()) + + require.Equal(t, + "ldaps://ldap.example.com?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev", + New(ProviderConfig{ + Host: "ldap.example.com", + UserSearch: UserSearchConfig{Base: "ou=users,dc=pinniped,dc=dev"}, + }).GetURL().String()) +} + +// 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 + + caForTestServerWithBadCertName, err := certauthority.New("Test CA", time.Hour) + require.NoError(t, err) + wrongIP := net.ParseIP("10.2.3.4") + cert, err := caForTestServerWithBadCertName.IssueServerCert([]string{"wrong-dns-name"}, []net.IP{wrongIP}, time.Hour) + require.NoError(t, err) + testServerWithBadCertNameAddr := testutil.TLSTestServerWithCert(t, func(w http.ResponseWriter, r *http.Request) {}, cert) + + 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 + connProto LDAPConnectionProtocol + caBundle []byte + context context.Context + wantError string + }{ + { + name: "happy path", + host: testServerHostAndPort, + caBundle: []byte(testServerCABundle), + connProto: TLS, + context: context.Background(), + }, + { + name: "server cert name does not match the address to which the client connected", + host: testServerWithBadCertNameAddr, + caBundle: caForTestServerWithBadCertName.Bundle(), + connProto: TLS, + context: context.Background(), + wantError: `LDAP Result Code 200 "Network Error": x509: certificate is valid for 10.2.3.4, not 127.0.0.1`, + }, + { + name: "invalid CA bundle with TLS", + host: testServerHostAndPort, + caBundle: []byte("not a ca bundle"), + connProto: TLS, + context: context.Background(), + wantError: `LDAP Result Code 200 "Network Error": could not parse CA bundle`, + }, + { + name: "invalid CA bundle with StartTLS", + host: testServerHostAndPort, + caBundle: []byte("not a ca bundle"), + connProto: StartTLS, + context: context.Background(), + wantError: `LDAP Result Code 200 "Network Error": could not parse CA bundle`, + }, + { + name: "invalid host with TLS", + host: "this:is:not:a:valid:hostname", + caBundle: []byte(testServerCABundle), + connProto: TLS, + context: context.Background(), + wantError: `LDAP Result Code 200 "Network Error": host "this:is:not:a:valid:hostname" is not a valid hostname or IP address`, + }, + { + name: "invalid host with StartTLS", + host: "this:is:not:a:valid:hostname", + caBundle: []byte(testServerCABundle), + connProto: StartTLS, + context: context.Background(), + wantError: `LDAP Result Code 200 "Network Error": host "this:is:not:a:valid:hostname" is not a valid hostname or IP address`, + }, + { + name: "missing CA bundle when it is required because the host is not using a trusted CA", + host: testServerHostAndPort, + caBundle: nil, + connProto: TLS, + 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), + connProto: TLS, + 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), + connProto: TLS, + context: alreadyCancelledContext, + wantError: fmt.Sprintf(`LDAP Result Code 200 "Network Error": dial tcp %s: operation was canceled`, testServerHostAndPort), + }, + { + name: "unsupported connection protocol", + host: testServerHostAndPort, + caBundle: []byte(testServerCABundle), + connProto: "bad usage of this type", + context: alreadyCancelledContext, + wantError: `LDAP Result Code 200 "Network Error": did not specify valid ConnectionProtocol`, + }, + } + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + provider := New(ProviderConfig{ + Host: tt.host, + CABundle: tt.caBundle, + ConnectionProtocol: tt.connProto, + Dialer: nil, // this test is for the default (production) TLS dialer + }) + conn, err := provider.dial(tt.context) + if conn != nil { + defer conn.Close() + } + if tt.wantError != "" { + require.Nil(t, conn) + require.EqualError(t, err, tt.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 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 3899292e891740dac78b01555cce0df8e18480c1 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Fri, 2 Jul 2021 15:30:27 -0700 Subject: [PATCH 02/36] Advertise Active Directory idps --- cmd/pinniped-supervisor/main.go | 11 + .../active_directory_upstream_watcher.go | 409 ++++++++ .../active_directory_upstream_watcher_test.go | 971 ++++++++++++++++++ internal/oidc/auth/auth_handler.go | 12 +- internal/oidc/auth/auth_handler_test.go | 21 + .../idpdiscovery/idp_discovery_handler.go | 8 +- .../idp_discovery_handler_test.go | 7 + internal/oidc/oidc.go | 5 + .../provider/dynamic_upstream_idp_provider.go | 26 +- .../testutil/oidctestutil/oidctestutil.go | 16 +- internal/upstreamad/upstreamad.go | 98 +- internal/upstreamad/upstreamad_test.go | 94 +- 12 files changed, 1538 insertions(+), 140 deletions(-) create mode 100644 internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go create mode 100644 internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 0cf897ae3..3ac3ad1e6 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -15,6 +15,8 @@ import ( "strings" "time" + "go.pinniped.dev/internal/controller/supervisorconfig/activedirectoryupstreamwatcher" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/clock" @@ -251,6 +253,15 @@ func startControllers( secretInformer, controllerlib.WithInformer, ), + singletonWorker). + WithController( + activedirectoryupstreamwatcher.New( + dynamicUpstreamIDPProvider, + pinnipedClient, + pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), + secretInformer, + controllerlib.WithInformer, + ), singletonWorker) kubeInformers.Start(ctx.Done()) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go new file mode 100644 index 000000000..5936c6bd8 --- /dev/null +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -0,0 +1,409 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package activedirectoryupstreamwatcher implements a controller which watches LDAPIdentityProviders. +package activedirectoryupstreamwatcher + +import ( + "context" + "crypto/x509" + "encoding/base64" + "fmt" + "time" + + "go.pinniped.dev/internal/upstreamad" + + 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" + 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/plog" + "go.pinniped.dev/internal/upstreamldap" +) + +const ( + activeDirectoryControllerName = "active-directory-upstream-observer" + activeDirectoryBindAccountSecretType = corev1.SecretTypeBasicAuth + testActiveDirectoryConnectionTimeout = 90 * time.Second + + // Constants related to conditions. + typeBindSecretValid = "BindSecretValid" + typeTLSConfigurationValid = "TLSConfigurationValid" + typeActiveDirectoryConnectionValid = "ActiveDirectoryConnectionValid" + reasonActiveDirectoryConnectionError = "ActiveDirectoryConnectionError" + noTLSConfigurationMessage = "no TLS configuration provided" + loadedTLSConfigurationMessage = "loaded TLS configuration" +) + +// UpstreamActiveDirectoryIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. +type UpstreamActiveDirectoryIdentityProviderICache interface { + SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) +} + +type activeDirectoryWatcherController struct { + cache UpstreamActiveDirectoryIdentityProviderICache + validatedSecretVersionsCache *secretVersionCache + ldapDialer upstreamldap.LDAPDialer + client pinnipedclientset.Interface + activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer + secretInformer corev1informers.SecretInformer +} + +// An in-memory cache with an entry for each ActiveDirectoryIdentityProvider, to keep track of which ResourceVersion +// of the bind Secret and which TLS/StartTLS setting was used during the most recent successful validation. +type secretVersionCache struct { + ValidatedSettingsByName map[string]validatedSettings +} + +type validatedSettings struct { + BindSecretResourceVersion string + LDAPConnectionProtocol upstreamldap.LDAPConnectionProtocol +} + +func newSecretVersionCache() *secretVersionCache { + return &secretVersionCache{ValidatedSettingsByName: map[string]validatedSettings{}} +} + +// New instantiates a new controllerlib.Controller which will populate the provided UpstreamActiveDirectoryIdentityProviderICache. +func New( + idpCache UpstreamActiveDirectoryIdentityProviderICache, + client pinnipedclientset.Interface, + activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer, + secretInformer corev1informers.SecretInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, +) controllerlib.Controller { + 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, + activeDirectoryIdentityProviderInformer, + secretInformer, + withInformer, + ) +} + +// For test dependency injection purposes. +func newInternal( + idpCache UpstreamActiveDirectoryIdentityProviderICache, + validatedSecretVersionsCache *secretVersionCache, + ldapDialer upstreamldap.LDAPDialer, + client pinnipedclientset.Interface, + activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer, + secretInformer corev1informers.SecretInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, +) controllerlib.Controller { + c := activeDirectoryWatcherController{ + cache: idpCache, + validatedSecretVersionsCache: validatedSecretVersionsCache, + ldapDialer: ldapDialer, + client: client, + activeDirectoryIdentityProviderInformer: activeDirectoryIdentityProviderInformer, + secretInformer: secretInformer, + } + return controllerlib.New( + controllerlib.Config{Name: activeDirectoryControllerName, Syncer: &c}, + withInformer( + activeDirectoryIdentityProviderInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + withInformer( + secretInformer, + pinnipedcontroller.MatchAnySecretOfTypeFilter(activeDirectoryBindAccountSecretType, pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + ) +} + +// Sync implements controllerlib.Syncer. +func (c *activeDirectoryWatcherController) Sync(ctx controllerlib.Context) error { + actualUpstreams, err := c.activeDirectoryIdentityProviderInformer.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, requestedRequeue := c.validateUpstream(ctx.Context, upstream) + if valid != nil { + validatedUpstreams = append(validatedUpstreams, valid) + } + if requestedRequeue { + requeue = true + } + } + + c.cache.SetActiveDirectoryIdentityProviders(validatedUpstreams) + + if requeue { + return controllerlib.ErrSyntheticRequeue + } + return nil +} + +func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) { + spec := upstream.Spec + + config := &upstreamldap.ProviderConfig{ + Name: upstream.Name, + Host: spec.Host, + UserSearch: upstreamldap.UserSearchConfig{ + Base: spec.UserSearch.Base, + Filter: spec.UserSearch.Filter, + UsernameAttribute: spec.UserSearch.Attributes.Username, + UIDAttribute: spec.UserSearch.Attributes.UID, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: spec.GroupSearch.Base, + Filter: spec.GroupSearch.Filter, + GroupNameAttribute: spec.GroupSearch.Attributes.GroupName, + }, + Dialer: c.ldapDialer, + } + + conditions := []*v1alpha1.Condition{} + 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. + var finishedConfigCondition *v1alpha1.Condition + if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { + finishedConfigCondition = c.validateFinishedConfig(ctx, upstream, config, currentSecretVersion) + if finishedConfigCondition != nil { + conditions = append(conditions, finishedConfigCondition) + } + } + + 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 = upstreamad.New(*config) + // Try again hoping that the condition will improve. + requeue = true + default: + // Fully validated provider, so load it into the cache. + p = upstreamad.New(*config) + requeue = false + } + + return p, requeue +} + +func (c *activeDirectoryWatcherController) validateTLSConfig(upstream *v1alpha1.ActiveDirectoryIdentityProvider, config *upstreamldap.ProviderConfig) *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", upstreamwatchers.ErrNoCertificates)) + } + + config.CABundle = bundle + return c.validTLSCondition(loadedTLSConfigurationMessage) +} + +func (c *activeDirectoryWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { + if c.hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion, config) { + return nil + } + + testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testActiveDirectoryConnectionTimeout) + defer cancelFunc() + + condition := c.testConnection(testConnectionTimeout, upstream, config, 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.ValidatedSettingsByName[upstream.GetName()] = validatedSettings{ + BindSecretResourceVersion: currentSecretVersion, + LDAPConnectionProtocol: config.ConnectionProtocol, + } + } + + return condition +} + +func (c *activeDirectoryWatcherController) testConnection( + ctx context.Context, + upstream *v1alpha1.ActiveDirectoryIdentityProvider, + config *upstreamldap.ProviderConfig, + currentSecretVersion string, +) *v1alpha1.Condition { + // First try using TLS. + config.ConnectionProtocol = upstreamldap.TLS + tlsLDAPProvider := upstreamldap.New(*config) + err := tlsLDAPProvider.TestConnection(ctx) + if err != nil { + plog.InfoErr("testing LDAP connection using TLS failed, so trying again with StartTLS", err, "host", config.Host) + // If there was any error, try again with StartTLS instead. + config.ConnectionProtocol = upstreamldap.StartTLS + startTLSLDAPProvider := upstreamldap.New(*config) + startTLSErr := startTLSLDAPProvider.TestConnection(ctx) + if startTLSErr == nil { + plog.Info("testing LDAP connection using StartTLS succeeded", "host", config.Host) + // Successfully able to fall back to using StartTLS, so clear the original + // error and consider the connection test to be successful. + err = nil + } else { + plog.InfoErr("testing LDAP connection using StartTLS also failed", err, "host", config.Host) + // Falling back to StartTLS also failed, so put TLS back into the config + // and consider the connection test to be failed. + config.ConnectionProtocol = upstreamldap.TLS + } + } + + if err != nil { + return &v1alpha1.Condition{ + Type: typeActiveDirectoryConnectionValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonActiveDirectoryConnectionError, + 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: typeActiveDirectoryConnectionValid, + Status: v1alpha1.ConditionTrue, + 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), + } +} + +func (c *activeDirectoryWatcherController) hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.ActiveDirectoryIdentityProvider, currentSecretVersion string, config *upstreamldap.ProviderConfig) bool { + currentGeneration := upstream.Generation + for _, cond := range upstream.Status.Conditions { + if cond.Type == typeActiveDirectoryConnectionValid && 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, if any. + validatedSecretVersion := c.validatedSecretVersionsCache.ValidatedSettingsByName[upstream.GetName()] + if validatedSecretVersion.BindSecretResourceVersion == currentSecretVersion { + // Reload the TLS vs StartTLS setting that was previously validated. + config.ConnectionProtocol = validatedSecretVersion.LDAPConnectionProtocol + return true + } + } + } + return false +} + +func (c *activeDirectoryWatcherController) validTLSCondition(message string) *v1alpha1.Condition { + return &v1alpha1.Condition{ + Type: typeTLSConfigurationValid, + Status: v1alpha1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: message, + } +} + +func (c *activeDirectoryWatcherController) invalidTLSCondition(message string) *v1alpha1.Condition { + return &v1alpha1.Condition{ + Type: typeTLSConfigurationValid, + Status: v1alpha1.ConditionFalse, + Reason: upstreamwatchers.ReasonInvalidTLSConfig, + Message: message, + } +} + +func (c *activeDirectoryWatcherController) validateSecret(upstream *v1alpha1.ActiveDirectoryIdentityProvider, config *upstreamldap.ProviderConfig) (*v1alpha1.Condition, string) { + secretName := upstream.Spec.Bind.SecretName + + secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) + if err != nil { + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + Status: v1alpha1.ConditionFalse, + Reason: upstreamwatchers.ReasonNotFound, + Message: err.Error(), + }, "" + } + + if secret.Type != corev1.SecretTypeBasicAuth { + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + Status: v1alpha1.ConditionFalse, + Reason: upstreamwatchers.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: upstreamwatchers.ReasonMissingKeys, + Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", + secretName, []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey}), + }, secret.ResourceVersion + } + + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + Status: v1alpha1.ConditionTrue, + Reason: upstreamwatchers.ReasonSuccess, + Message: "loaded bind secret", + }, secret.ResourceVersion +} + +func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider, conditions []*v1alpha1.Condition) { + log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) + updated := upstream.DeepCopy() + + hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) + + updated.Status.Phase = v1alpha1.ActiveDirectoryPhaseReady + if hadErrorCondition { + updated.Status.Phase = v1alpha1.ActiveDirectoryPhaseError + } + + if equality.Semantic.DeepEqual(upstream, updated) { + return // nothing to update + } + + _, err := c.client. + IDPV1alpha1(). + ActiveDirectoryIdentityProviders(upstream.Namespace). + UpdateStatus(ctx, updated, metav1.UpdateOptions{}) + if err != nil { + log.Error(err, "failed to update status") + } +} diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go new file mode 100644 index 000000000..1b7ae2aae --- /dev/null +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -0,0 +1,971 @@ +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package activedirectoryupstreamwatcher + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "sort" + "testing" + "time" + + "go.pinniped.dev/internal/upstreamad" + + "github.com/go-ldap/ldap/v3" + "github.com/golang/mock/gomock" + "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/certauthority" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/endpointaddr" + "go.pinniped.dev/internal/mocks/mockldapconn" + "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) + activeDirectoryIDPInformer := pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders() + fakeKubeClient := fake.NewSimpleClientset() + kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) + secretInformer := kubeInformers.Core().V1().Secrets() + withInformer := testutil.NewObservableWithInformerOption() + + New(nil, nil, activeDirectoryIDPInformer, 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.ActiveDirectoryIdentityProvider{ + 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) + activeDirectoryIDPInformer := pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders() + fakeKubeClient := fake.NewSimpleClientset() + kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) + secretInformer := kubeInformers.Core().V1().Secrets() + withInformer := testutil.NewObservableWithInformerOption() + + New(nil, nil, activeDirectoryIDPInformer, secretInformer, withInformer.WithInformer) + + unrelated := corev1.Secret{} + filter := withInformer.GetFilterForInformer(activeDirectoryIDPInformer) + 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)) + }) + } +} + +// Wrap the func into a struct so the test can do deep equal assertions on instances of upstreamad.Provider. +type comparableDialer struct { + upstreamldap.LDAPDialerFunc +} + +func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { + t.Parallel() + now := metav1.NewTime(time.Now().UTC()) + + const ( + testNamespace = "test-namespace" + testName = "test-name" + testSecretName = "test-bind-secret" + testBindUsername = "test-bind-username" + testBindPassword = "test-bind-password" + testHost = "ldap.example.com:123" + testUserSearchBase = "test-user-search-base" + testUserSearchFilter = "test-user-search-filter" + testGroupSearchBase = "test-group-search-base" + testGroupSearchFilter = "test-group-search-filter" + testUsernameAttrName = "test-username-attr" + testGroupNameAttrName = "test-group-name-attr" + testUIDAttrName = "test-uid-attr" + ) + + 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) + + validUpstream := &v1alpha1.ActiveDirectoryIdentityProvider{ + ObjectMeta: metav1.ObjectMeta{Name: testName, Namespace: testNamespace, Generation: 1234}, + Spec: v1alpha1.ActiveDirectoryIdentityProviderSpec{ + Host: testHost, + TLS: &v1alpha1.TLSSpec{CertificateAuthorityData: testCABundleBase64Encoded}, + Bind: v1alpha1.ActiveDirectoryIdentityProviderBind{SecretName: testSecretName}, + UserSearch: v1alpha1.ActiveDirectoryIdentityProviderUserSearch{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + Attributes: v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{ + Username: testUsernameAttrName, + UID: testUIDAttrName, + }, + }, + GroupSearch: v1alpha1.ActiveDirectoryIdentityProviderGroupSearch{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + Attributes: v1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{ + GroupName: testGroupNameAttrName, + }, + }, + }, + } + editedValidUpstream := func(editFunc func(*v1alpha1.ActiveDirectoryIdentityProvider)) *v1alpha1.ActiveDirectoryIdentityProvider { + deepCopy := validUpstream.DeepCopy() + editFunc(deepCopy) + return deepCopy + } + + providerConfigForValidUpstreamWithTLS := &upstreamldap.ProviderConfig{ + Name: testName, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + } + + // Make a copy with targeted changes. + copyOfProviderConfigForValidUpstreamWithTLS := *providerConfigForValidUpstreamWithTLS + providerConfigForValidUpstreamWithStartTLS := ©OfProviderConfigForValidUpstreamWithTLS + providerConfigForValidUpstreamWithStartTLS.ConnectionProtocol = upstreamldap.StartTLS + + bindSecretValidTrueCondition := func(gen int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "BindSecretValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "loaded bind secret", + ObservedGeneration: gen, + } + } + activeDirectoryConnectionValidTrueCondition := func(gen int64, secretVersion string) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "ActiveDirectoryConnectionValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + 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, + } + } + 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, secretVersion string) []v1alpha1.Condition { + return []v1alpha1.Condition{ + activeDirectoryConnectionValidTrueCondition(gen, secretVersion), + bindSecretValidTrueCondition(gen), + 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 + initialValidatedSettings map[string]validatedSettings + inputUpstreams []runtime.Object + inputSecrets []runtime.Object + setupMocks func(conn *mockldapconn.MockConn) + dialErrors map[string]error + wantErr string + wantResultingCache []*upstreamldap.ProviderConfig + wantResultingUpstreams []v1alpha1.ActiveDirectoryIdentityProvider + wantValidatedSettings map[string]validatedSettings + }{ + { + name: "no ActiveDirectoryIdentityProvider upstreams clears the cache", + wantResultingCache: []*upstreamldap.ProviderConfig{}, + }, + { + name: "one valid upstream updates the cache to include only that upstream", + inputUpstreams: []runtime.Object{validUpstream}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + 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{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, + { + name: "missing secret", + inputUpstreams: []runtime.Object{validUpstream}, + inputSecrets: []runtime.Object{}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "BindSecretValid", + Status: "False", + LastTransitionTime: now, + Reason: "SecretNotFound", + Message: fmt.Sprintf(`secret "%s" not found`, testSecretName), + ObservedGeneration: 1234, + }, + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + }, + { + name: "secret has wrong type", + 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.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + 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, + }, + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + }, + { + name: "secret is missing key", + 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.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + 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, + }, + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + }, + { + name: "CertificateAuthorityData is not base64 encoded", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.TLS.CertificateAuthorityData = "this-is-not-base64-encoded" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("")}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(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", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.TLS.CertificateAuthorityData = base64.StdEncoding.EncodeToString([]byte("this is not pem data")) + })}, + inputSecrets: []runtime.Object{validBindUserSecret("")}, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + { + Type: "TLSConfigurationValid", + Status: "False", + LastTransitionTime: now, + Reason: "InvalidTLSConfig", + Message: "certificateAuthorityData is invalid: no certificates found", + ObservedGeneration: 1234, + }, + }, + }, + }}, + }, + { + name: "nil TLS configuration is valid", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.TLS = nil + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + 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, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: nil, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + bindSecretValidTrueCondition(1234), + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "no TLS configuration provided", + ObservedGeneration: 1234, + }, + }, + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, + { + name: "when TLS connection fails it tries to use StartTLS instead: without a specified port it automatically switches ports", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.Host = "ldap.example.com" // when the port is not specified, automatically switch ports for StartTLS + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + dialErrors: map[string]error{ + "ldap.example.com:" + ldap.DefaultLdapsPort: fmt.Errorf("some ldaps dial error"), + "ldap.example.com:" + ldap.DefaultLdapPort: nil, // no error on the regular ldap:// port + }, + wantResultingCache: []*upstreamldap.ProviderConfig{ + { + Name: testName, + Host: "ldap.example.com", + ConnectionProtocol: upstreamldap.StartTLS, // successfully fell back to using StartTLS + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + { + Type: "ActiveDirectoryConnectionValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + "ldap.example.com", testBindUsername, testSecretName, "4242"), + ObservedGeneration: 1234, + }, + bindSecretValidTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + }, + { + name: "when TLS connection fails it tries to use StartTLS instead: with a specified port it does not automatically switch ports", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.Host = "ldap.example.com:5678" // when the port is specified, do not automatically switch ports for StartTLS + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Both dials fail, so there should be no bind. + }, + dialErrors: map[string]error{ + "ldap.example.com:5678": fmt.Errorf("some dial error"), // both TLS and StartTLS should try the same port and both fail + }, + wantResultingCache: []*upstreamldap.ProviderConfig{ + // even though the connection test failed, still loads into the cache because it is treated like a warning + { + Name: testName, + Host: "ldap.example.com:5678", + ConnectionProtocol: upstreamldap.TLS, // need to pick TLS or StartTLS to load into the cache when both fail, so choose TLS + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "ActiveDirectoryConnectionValid", + Status: "False", + LastTransitionTime: now, + Reason: "ActiveDirectoryConnectionError", + Message: fmt.Sprintf( + `could not successfully connect to "%s" and bind as user "%s": error dialing host "%s": some dial error`, + "ldap.example.com:5678", testBindUsername, "ldap.example.com:5678"), + ObservedGeneration: 1234, + }, + bindSecretValidTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + }, + { + name: "non-nil TLS configuration with empty CertificateAuthorityData is valid", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.TLS.CertificateAuthorityData = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + 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, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: nil, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, + { + name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", + inputUpstreams: []runtime.Object{validUpstream, editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Name = "other-upstream" + upstream.Generation = 42 + upstream.Spec.Bind.SecretName = "non-existent-secret" + })}, + 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) + conn.EXPECT().Close().Times(1) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{ + { + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "other-upstream", Generation: 42}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + 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, + }, + tlsConfigurationValidLoadedTrueCondition(42), + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }, + }, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, + { + 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) { + // Should perform a test dial and bind. + // Expect two calls to each of these: once for trying TLS and once for trying StartTLS. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2).Return(errors.New("some bind error")) + conn.EXPECT().Close().Times(2) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + { + Type: "ActiveDirectoryConnectionValid", + Status: "False", + LastTransitionTime: now, + Reason: "ActiveDirectoryConnectionError", + 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, + }, + bindSecretValidTrueCondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + }, + { + name: "when the LDAP server connection was already validated using TLS for the current resource generation and secret version, then do not validate it again and keep using TLS", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + } + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + 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{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, + { + name: "when the LDAP server connection was already validated using StartTLS for the current resource generation and secret version, then do not validate it again and keep using StartTLS", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + } + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + 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{providerConfigForValidUpstreamWithStartTLS}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + }, + { + 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.ActiveDirectoryIdentityProvider) { + upstream.Generation = 1234 // current generation + upstream.Status.Conditions = []v1alpha1.Condition{ + activeDirectoryConnectionValidTrueCondition(1233, "4242"), // older spec generation! + } + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + 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{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, + { + 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.ActiveDirectoryIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + { + Type: "ActiveDirectoryConnectionValid", + Status: "False", // failure! + LastTransitionTime: now, + Reason: "ActiveDirectoryConnectionError", + Message: "some-error-message", + ObservedGeneration: 1234, // same (current) generation! + }, + } + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "1", LDAPConnectionProtocol: upstreamldap.TLS}}, + 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{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, + { + 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.ActiveDirectoryIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + activeDirectoryConnectionValidTrueCondition(1234, "4241"), // same spec generation, old secret version + } + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! + initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS}}, // old version was validated + 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{providerConfigForValidUpstreamWithTLS}, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, + } + + 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.SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + upstreamad.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{upstreamldap.LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { + if tt.dialErrors != nil { + dialErr := tt.dialErrors[addr.Endpoint()] + if dialErr != nil { + return nil, dialErr + } + } + return conn, nil + })} + + validatedSecretVersionCache := newSecretVersionCache() + if tt.initialValidatedSettings != nil { + validatedSecretVersionCache.ValidatedSettingsByName = tt.initialValidatedSettings + } + + controller := newInternal( + cache, + validatedSecretVersionCache, + dialer, + fakePinnipedClient, + pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(), + 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.GetActiveDirectoryIdentityProviders() + require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) + for i := range actualIDPList { + actualIDP := actualIDPList[i].(*upstreamad.Provider) + copyOfExpectedValueForResultingCache := *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. + copyOfExpectedValueForResultingCache.Dialer = dialer + require.Equal(t, copyOfExpectedValueForResultingCache, actualIDP.GetConfig()) + } + + actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().ActiveDirectoryIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + + // Assert on the expected Status of the upstreams. Preprocess the upstreams a bit so that they're easier to assert against. + normalizedActualUpstreams := normalizeActiveDirectoryUpstreams(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]) + } + + // Check that the controller remembered which version of the secret it most recently validated successfully with. + if tt.wantValidatedSettings == nil { + tt.wantValidatedSettings = map[string]validatedSettings{} + } + require.Equal(t, tt.wantValidatedSettings, validatedSecretVersionCache.ValidatedSettingsByName) + }) + } +} + +func normalizeActiveDirectoryUpstreams(upstreams []v1alpha1.ActiveDirectoryIdentityProvider, now metav1.Time) []v1alpha1.ActiveDirectoryIdentityProvider { + result := make([]v1alpha1.ActiveDirectoryIdentityProvider, 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.ActiveDirectoryIdentityProviderSpec{} + + // 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) + } + + sort.SliceStable(result, func(i, j int) bool { + return result[i].Name < result[j].Name + }) + + return result +} diff --git a/internal/oidc/auth/auth_handler.go b/internal/oidc/auth/auth_handler.go index 0f21ad051..87ebe8763 100644 --- a/internal/oidc/auth/auth_handler.go +++ b/internal/oidc/auth/auth_handler.go @@ -260,17 +260,18 @@ func readCSRFCookie(r *http.Request, codec oidc.Decoder) csrftoken.CSRFToken { return csrfFromCookie } -// Select either an OIDC or an LDAP IDP, or return an error. +// Select either an OIDC, an LDAP or an AD IDP, or return an error. func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider.UpstreamOIDCIdentityProviderI, provider.UpstreamLDAPIdentityProviderI, error) { oidcUpstreams := idpLister.GetOIDCIdentityProviders() ldapUpstreams := idpLister.GetLDAPIdentityProviders() + adUpstreams := idpLister.GetActiveDirectoryIdentityProviders() switch { - case len(oidcUpstreams)+len(ldapUpstreams) == 0: + case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) == 0: return nil, nil, httperr.New( http.StatusUnprocessableEntity, "No upstream providers are configured", ) - case len(oidcUpstreams)+len(ldapUpstreams) > 1: + case len(oidcUpstreams)+len(ldapUpstreams)+len(adUpstreams) > 1: var upstreamIDPNames []string for _, idp := range oidcUpstreams { upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) @@ -278,6 +279,9 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider for _, idp := range ldapUpstreams { upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) } + for _, idp := range adUpstreams { + upstreamIDPNames = append(upstreamIDPNames, idp.GetName()) + } plog.Warning("Too many upstream providers are configured (found: %s)", upstreamIDPNames) return nil, nil, httperr.New( http.StatusUnprocessableEntity, @@ -285,6 +289,8 @@ func chooseUpstreamIDP(idpLister oidc.UpstreamIdentityProvidersLister) (provider ) case len(oidcUpstreams) == 1: return oidcUpstreams[0], nil, nil + case len(adUpstreams) == 1: + return nil, adUpstreams[0], nil default: return nil, ldapUpstreams[0], nil } diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index e6917954f..9a4a99efc 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -397,6 +397,27 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, }, + { + name: "ActiveDirectory upstream happy path using GET", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.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(), diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler.go b/internal/oidc/idpdiscovery/idp_discovery_handler.go index 9ee0bf767..8bb6f0051 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler.go @@ -14,8 +14,9 @@ import ( ) const ( - idpDiscoveryTypeLDAP = "ldap" - idpDiscoveryTypeOIDC = "oidc" + idpDiscoveryTypeLDAP = "ldap" + idpDiscoveryTypeOIDC = "oidc" + idpDiscoveryTypeActiveDirectory = "activedirectory" ) type response struct { @@ -58,6 +59,9 @@ func responseAsJSON(upstreamIDPs oidc.UpstreamIdentityProvidersLister) ([]byte, for _, provider := range upstreamIDPs.GetLDAPIdentityProviders() { r.IDPs = append(r.IDPs, identityProviderResponse{Name: provider.GetName(), Type: idpDiscoveryTypeLDAP}) } + for _, provider := range upstreamIDPs.GetActiveDirectoryIdentityProviders() { + r.IDPs = append(r.IDPs, identityProviderResponse{Name: provider.GetName(), Type: idpDiscoveryTypeActiveDirectory}) + } for _, provider := range upstreamIDPs.GetOIDCIdentityProviders() { r.IDPs = append(r.IDPs, identityProviderResponse{Name: provider.GetName(), Type: idpDiscoveryTypeOIDC}) } diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go index 3912f9c99..f48368189 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go @@ -41,12 +41,14 @@ func TestIDPDiscovery(t *testing.T) { {Name: "a-some-oidc-idp", Type: "oidc"}, {Name: "x-some-idp", Type: "ldap"}, {Name: "x-some-idp", Type: "oidc"}, + {Name: "z-some-ad-idp", Type: "activedirectory"}, {Name: "z-some-ldap-idp", Type: "ldap"}, {Name: "z-some-oidc-idp", Type: "oidc"}, }, }, wantSecondResponseBodyJSON: &response{ IDPs: []identityProviderResponse{ + {Name: "some-other-ad-idp-1", Type: "activedirectory"}, {Name: "some-other-ldap-idp-1", Type: "ldap"}, {Name: "some-other-ldap-idp-2", Type: "ldap"}, {Name: "some-other-oidc-idp-1", Type: "oidc"}, @@ -73,6 +75,7 @@ func TestIDPDiscovery(t *testing.T) { WithOIDC(&oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "a-some-oidc-idp"}). WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ldap-idp"}). WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "x-some-idp"}). + WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ad-idp"}). Build() handler := NewHandler(idpLister) @@ -104,6 +107,10 @@ func TestIDPDiscovery(t *testing.T) { &oidctestutil.TestUpstreamOIDCIdentityProvider{Name: "some-other-oidc-idp-2"}, }) + idpLister.SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-1"}, + }) + // Make the same request to the same handler instance again, and expect different results. rsp = httptest.NewRecorder() handler.ServeHTTP(rsp, req) diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go index d29979c84..e5038689c 100644 --- a/internal/oidc/oidc.go +++ b/internal/oidc/oidc.go @@ -267,9 +267,14 @@ type UpstreamLDAPIdentityProvidersLister interface { GetLDAPIdentityProviders() []provider.UpstreamLDAPIdentityProviderI } +type UpstreamActiveDirectoryIdentityProviderLister interface { + GetActiveDirectoryIdentityProviders() []provider.UpstreamLDAPIdentityProviderI +} + type UpstreamIdentityProvidersLister interface { UpstreamOIDCIdentityProvidersLister UpstreamLDAPIdentityProvidersLister + UpstreamActiveDirectoryIdentityProviderLister } func GrantScopeIfRequested(authorizeRequester fosite.AuthorizeRequester, scopeName string) { diff --git a/internal/oidc/provider/dynamic_upstream_idp_provider.go b/internal/oidc/provider/dynamic_upstream_idp_provider.go index af24e7244..5f0c39f1b 100644 --- a/internal/oidc/provider/dynamic_upstream_idp_provider.go +++ b/internal/oidc/provider/dynamic_upstream_idp_provider.go @@ -67,18 +67,22 @@ type DynamicUpstreamIDPProvider interface { GetOIDCIdentityProviders() []UpstreamOIDCIdentityProviderI SetLDAPIdentityProviders(ldapIDPs []UpstreamLDAPIdentityProviderI) GetLDAPIdentityProviders() []UpstreamLDAPIdentityProviderI + SetActiveDirectoryIdentityProviders(adIDPs []UpstreamLDAPIdentityProviderI) + GetActiveDirectoryIdentityProviders() []UpstreamLDAPIdentityProviderI } type dynamicUpstreamIDPProvider struct { - oidcUpstreams []UpstreamOIDCIdentityProviderI - ldapUpstreams []UpstreamLDAPIdentityProviderI - mutex sync.RWMutex + oidcUpstreams []UpstreamOIDCIdentityProviderI + ldapUpstreams []UpstreamLDAPIdentityProviderI + activeDirectoryUpstreams []UpstreamLDAPIdentityProviderI + mutex sync.RWMutex } func NewDynamicUpstreamIDPProvider() DynamicUpstreamIDPProvider { return &dynamicUpstreamIDPProvider{ - oidcUpstreams: []UpstreamOIDCIdentityProviderI{}, - ldapUpstreams: []UpstreamLDAPIdentityProviderI{}, + oidcUpstreams: []UpstreamOIDCIdentityProviderI{}, + ldapUpstreams: []UpstreamLDAPIdentityProviderI{}, + activeDirectoryUpstreams: []UpstreamLDAPIdentityProviderI{}, } } @@ -105,3 +109,15 @@ func (p *dynamicUpstreamIDPProvider) GetLDAPIdentityProviders() []UpstreamLDAPId defer p.mutex.RUnlock() return p.ldapUpstreams } + +func (p *dynamicUpstreamIDPProvider) SetActiveDirectoryIdentityProviders(adIDPs []UpstreamLDAPIdentityProviderI) { + p.mutex.Lock() // acquire a write lock + defer p.mutex.Unlock() + p.activeDirectoryUpstreams = adIDPs +} + +func (p *dynamicUpstreamIDPProvider) GetActiveDirectoryIdentityProviders() []UpstreamLDAPIdentityProviderI { + p.mutex.RLock() // acquire a read lock + defer p.mutex.RUnlock() + return p.activeDirectoryUpstreams +} diff --git a/internal/testutil/oidctestutil/oidctestutil.go b/internal/testutil/oidctestutil/oidctestutil.go index f690af520..9bed018e0 100644 --- a/internal/testutil/oidctestutil/oidctestutil.go +++ b/internal/testutil/oidctestutil/oidctestutil.go @@ -148,8 +148,9 @@ func (u *TestUpstreamOIDCIdentityProvider) ValidateToken(_ context.Context, _ *o } type UpstreamIDPListerBuilder struct { - upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider - upstreamLDAPIdentityProviders []*TestUpstreamLDAPIdentityProvider + upstreamOIDCIdentityProviders []*TestUpstreamOIDCIdentityProvider + upstreamLDAPIdentityProviders []*TestUpstreamLDAPIdentityProvider + upstreamActiveDirectoryIdentityProviders []*TestUpstreamLDAPIdentityProvider } func (b *UpstreamIDPListerBuilder) WithOIDC(upstreamOIDCIdentityProviders ...*TestUpstreamOIDCIdentityProvider) *UpstreamIDPListerBuilder { @@ -162,6 +163,11 @@ func (b *UpstreamIDPListerBuilder) WithLDAP(upstreamLDAPIdentityProviders ...*Te return b } +func (b *UpstreamIDPListerBuilder) WithActiveDirectory(upstreamActiveDirectoryIdentityProviders ...*TestUpstreamLDAPIdentityProvider) *UpstreamIDPListerBuilder { + b.upstreamActiveDirectoryIdentityProviders = append(b.upstreamActiveDirectoryIdentityProviders, upstreamActiveDirectoryIdentityProviders...) + return b +} + func (b *UpstreamIDPListerBuilder) Build() provider.DynamicUpstreamIDPProvider { idpProvider := provider.NewDynamicUpstreamIDPProvider() @@ -177,6 +183,12 @@ func (b *UpstreamIDPListerBuilder) Build() provider.DynamicUpstreamIDPProvider { } idpProvider.SetLDAPIdentityProviders(ldapUpstreams) + adUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, len(b.upstreamActiveDirectoryIdentityProviders)) + for i := range b.upstreamActiveDirectoryIdentityProviders { + adUpstreams[i] = provider.UpstreamLDAPIdentityProviderI(b.upstreamActiveDirectoryIdentityProviders[i]) + } + idpProvider.SetActiveDirectoryIdentityProviders(adUpstreams) + return idpProvider } diff --git a/internal/upstreamad/upstreamad.go b/internal/upstreamad/upstreamad.go index eff0a9f90..37fe3912b 100644 --- a/internal/upstreamad/upstreamad.go +++ b/internal/upstreamad/upstreamad.go @@ -17,6 +17,8 @@ import ( "strings" "time" + "go.pinniped.dev/internal/upstreamldap" + "github.com/go-ldap/ldap/v3" "github.com/gofrs/uuid" "k8s.io/apiserver/pkg/authentication/authenticator" @@ -40,74 +42,6 @@ const ( defaultLDAPSPort = uint16(636) ) -// Conn abstracts the upstream LDAP communication protocol (mostly for testing). -type Conn interface { - Bind(username, password string) error - - Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) - - SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) - - Close() -} - -// Our Conn type is subset of the ldap.Client interface, which is implemented by ldap.Conn. -var _ Conn = &ldap.Conn{} - -// 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, addr endpointaddr.HostPort) (Conn, error) -} - -// LDAPDialerFunc makes it easy to use a func as an LDAPDialer. -type LDAPDialerFunc func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) - -var _ LDAPDialer = LDAPDialerFunc(nil) - -func (f LDAPDialerFunc) Dial(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) { - return f(ctx, addr) -} - -type LDAPConnectionProtocol string - -const ( - StartTLS = LDAPConnectionProtocol("StartTLS") - TLS = LDAPConnectionProtocol("TLS") -) - -// 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. -// 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 - - // 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 - - // ConnectionProtocol determines how to establish the connection to the server. Either StartTLS or TLS. - ConnectionProtocol LDAPConnectionProtocol - - // 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 active directory IDP. - BindUsername string - - // BindPassword is the password to use when performing a bind with the upstream active directory IDP. - BindPassword string - - // UserSearch contains information about how to search for users in the upstream active directory IDP. - UserSearch UserSearchConfig - - // GroupSearch contains information about how to search for group membership in the upstream active directory IDP. - GroupSearch GroupSearchConfig - - // Dialer exists to enable testing. When nil, will use a default appropriate for production use. - Dialer LDAPDialer -} - // UserSearchConfig contains information about how to search for users in the upstream active directory IDP. type UserSearchConfig struct { // Base is the base DN to use for the user search in the upstream active directory IDP. @@ -140,7 +74,7 @@ type GroupSearchConfig struct { } type Provider struct { - c ProviderConfig + c upstreamldap.ProviderConfig } var _ provider.UpstreamLDAPIdentityProviderI = &Provider{} @@ -148,16 +82,16 @@ var _ authenticators.UserAuthenticator = &Provider{} // 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 { +func New(config upstreamldap.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 { +func (p *Provider) GetConfig() upstreamldap.ProviderConfig { return p.c } -func (p *Provider) dial(ctx context.Context) (Conn, error) { +func (p *Provider) dial(ctx context.Context) (upstreamldap.Conn, error) { tlsAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPSPort) if err != nil { return nil, ldap.NewError(ldap.ErrorNetwork, err) @@ -169,13 +103,13 @@ func (p *Provider) dial(ctx context.Context) (Conn, error) { } // Choose how and where to dial based on TLS vs. StartTLS config option. - var dialFunc LDAPDialerFunc + var dialFunc upstreamldap.LDAPDialerFunc var addr endpointaddr.HostPort switch { - case p.c.ConnectionProtocol == TLS: + case p.c.ConnectionProtocol == upstreamldap.TLS: dialFunc = p.dialTLS addr = tlsAddr - case p.c.ConnectionProtocol == StartTLS: + case p.c.ConnectionProtocol == upstreamldap.StartTLS: dialFunc = p.dialStartTLS addr = startTLSAddr default: @@ -193,7 +127,7 @@ func (p *Provider) dial(ctx context.Context) (Conn, error) { // dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is TLS. // 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, addr endpointaddr.HostPort) (Conn, error) { +func (p *Provider) dialTLS(ctx context.Context, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { tlsConfig, err := p.tlsConfig() if err != nil { return nil, ldap.NewError(ldap.ErrorNetwork, err) @@ -213,7 +147,7 @@ func (p *Provider) dialTLS(ctx context.Context, addr endpointaddr.HostPort) (Con // dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is StartTLS. // 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) dialStartTLS(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) { +func (p *Provider) dialStartTLS(ctx context.Context, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { tlsConfig, err := p.tlsConfig() if err != nil { return nil, ldap.NewError(ldap.ErrorNetwork, err) @@ -295,7 +229,7 @@ func (p *Provider) TestConnection(ctx context.Context) error { // not bind as that user, so it does not test their password. It returns the same values that a real call to // AuthenticateUser with the correct password would return. func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticator.Response, bool, error) { - endUserBindFunc := func(conn Conn, foundUserDN string) error { + endUserBindFunc := func(conn upstreamldap.Conn, foundUserDN string) error { // Act as if the end user bind always succeeds. return nil } @@ -304,13 +238,13 @@ func (p *Provider) DryRunAuthenticateUser(ctx context.Context, username string) // Authenticate an end user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator. func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { - endUserBindFunc := func(conn Conn, foundUserDN string) error { + endUserBindFunc := func(conn upstreamldap.Conn, foundUserDN string) error { return conn.Bind(foundUserDN, password) } return p.authenticateUserImpl(ctx, username, endUserBindFunc) } -func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn Conn, foundUserDN string) error) (*authenticator.Response, bool, error) { +func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bindFunc func(conn upstreamldap.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 @@ -361,7 +295,7 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi return response, true, nil } -func (p *Provider) searchGroupsForUserDN(conn Conn, userDN string) ([]string, error) { +func (p *Provider) searchGroupsForUserDN(conn upstreamldap.Conn, userDN string) ([]string, error) { searchResult, err := conn.SearchWithPaging(p.groupSearchRequest(userDN), groupSearchPageSize) if err != nil { return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err) @@ -396,7 +330,7 @@ func (p *Provider) validateConfig() error { return nil } -func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, []string, error) { +func (p *Provider) searchAndBindUser(conn upstreamldap.Conn, username string, bindFunc func(conn upstreamldap.Conn, foundUserDN string) error) (string, string, []string, error) { searchResult, err := conn.Search(p.userSearchRequest(username)) if err != nil { plog.All(`error searching for user`, diff --git a/internal/upstreamad/upstreamad_test.go b/internal/upstreamad/upstreamad_test.go index a518bae21..1fe0fca82 100644 --- a/internal/upstreamad/upstreamad_test.go +++ b/internal/upstreamad/upstreamad_test.go @@ -15,6 +15,8 @@ import ( "testing" "time" + "go.pinniped.dev/internal/upstreamldap" + "github.com/go-ldap/ldap/v3" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" @@ -57,21 +59,21 @@ var ( ) func TestEndUserAuthentication(t *testing.T) { - providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig { - config := &ProviderConfig{ + providerConfig := func(editFunc func(p *upstreamldap.ProviderConfig)) *upstreamldap.ProviderConfig { + config := &upstreamldap.ProviderConfig{ Name: "some-provider-name", Host: testHost, CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: TLS, + ConnectionProtocol: upstreamldap.TLS, BindUsername: testBindUsername, BindPassword: testBindPassword, - UserSearch: UserSearchConfig{ + UserSearch: upstreamldap.UserSearchConfig{ Base: testUserSearchBase, Filter: testUserSearchFilter, UsernameAttribute: testUserSearchUsernameAttribute, UIDAttribute: testUserSearchUIDAttribute, }, - GroupSearch: GroupSearchConfig{ + GroupSearch: upstreamldap.GroupSearchConfig{ Base: testGroupSearchBase, Filter: testGroupSearchFilter, GroupNameAttribute: testGroupSearchGroupNameAttribute, @@ -167,7 +169,7 @@ func TestEndUserAuthentication(t *testing.T) { name string username string password string - providerConfig *ProviderConfig + providerConfig *upstreamldap.ProviderConfig searchMocks func(conn *mockldapconn.MockConn) bindEndUserMocks func(conn *mockldapconn.MockConn) dialError error @@ -198,14 +200,14 @@ func TestEndUserAuthentication(t *testing.T) { name: "default as much as possible", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: &ProviderConfig{ + providerConfig: &upstreamldap.ProviderConfig{ Name: "some-provider-name", Host: testHost, CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: TLS, + ConnectionProtocol: upstreamldap.TLS, BindUsername: testBindUsername, BindPassword: testBindPassword, - GroupSearch: GroupSearchConfig{ + GroupSearch: upstreamldap.GroupSearchConfig{ Base: testGroupSearchBase, Filter: testGroupSearchFilter, GroupNameAttribute: testGroupSearchGroupNameAttribute, @@ -241,7 +243,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the user search filter is already wrapped by parenthesis then it is not wrapped again", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(" + testUserSearchFilter + ")" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -260,7 +262,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the group search filter is already wrapped by parenthesis then it is not wrapped again", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Filter = "(" + testGroupSearchFilter + ")" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -279,7 +281,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the group search base is empty then skip the group search entirely", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Base = "" // this configuration means that the user does not want group search to happen }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -298,7 +300,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the UsernameAttribute is dn and there is a user search filter provided", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "dn" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -330,7 +332,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the UIDAttribute is dn", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -362,7 +364,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the GroupNameAttribute is empty then it defaults to dn", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.GroupNameAttribute = "" // blank means to use dn }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -385,7 +387,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the GroupNameAttribute is dn", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.GroupNameAttribute = "dn" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -408,7 +410,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the GroupNameAttribute is cn", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.GroupNameAttribute = "cn" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -446,7 +448,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when user search Filter is blank it derives a search filter from the UsernameAttribute", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -467,7 +469,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when user search Filter and user attribute is blank it defaults to sAMAccountName={}", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "" p.UserSearch.UsernameAttribute = "" }), @@ -500,7 +502,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when group search Filter is blank it uses a default search filter of member={}", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Filter = "" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -594,7 +596,7 @@ func TestEndUserAuthentication(t *testing.T) { name: "when the UsernameAttribute is dn and there is not a user search filter provided", username: testUpstreamUsername, password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "dn" p.UserSearch.Filter = "" }), @@ -1027,7 +1029,7 @@ func TestEndUserAuthentication(t *testing.T) { } dialWasAttempted := false - tt.providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) { + tt.providerConfig.Dialer = upstreamldap.LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { dialWasAttempted = true require.Equal(t, tt.providerConfig.Host, addr.Endpoint()) if tt.dialError != nil { @@ -1091,15 +1093,15 @@ func TestEndUserAuthentication(t *testing.T) { } func TestTestConnection(t *testing.T) { - providerConfig := func(editFunc func(p *ProviderConfig)) *ProviderConfig { - config := &ProviderConfig{ + providerConfig := func(editFunc func(p *upstreamldap.ProviderConfig)) *upstreamldap.ProviderConfig { + config := &upstreamldap.ProviderConfig{ Name: "some-provider-name", Host: testHost, CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: TLS, + ConnectionProtocol: upstreamldap.TLS, BindUsername: testBindUsername, BindPassword: testBindPassword, - UserSearch: UserSearchConfig{}, // not used by TestConnection + UserSearch: upstreamldap.UserSearchConfig{}, // not used by TestConnection } if editFunc != nil { editFunc(config) @@ -1109,7 +1111,7 @@ func TestTestConnection(t *testing.T) { tests := []struct { name string - providerConfig *ProviderConfig + providerConfig *upstreamldap.ProviderConfig setupMocks func(conn *mockldapconn.MockConn) dialError error wantError string @@ -1140,7 +1142,7 @@ func TestTestConnection(t *testing.T) { }, { name: "when the config is invalid", - providerConfig: providerConfig(func(p *ProviderConfig) { + providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { // This particular combination of options is not allowed. p.UserSearch.UsernameAttribute = "dn" p.UserSearch.Filter = "" @@ -1162,7 +1164,7 @@ func TestTestConnection(t *testing.T) { } dialWasAttempted := false - tt.providerConfig.Dialer = LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (Conn, error) { + tt.providerConfig.Dialer = upstreamldap.LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { dialWasAttempted = true require.Equal(t, tt.providerConfig.Host, addr.Endpoint()) if tt.dialError != nil { @@ -1187,13 +1189,13 @@ func TestTestConnection(t *testing.T) { } func TestGetConfig(t *testing.T) { - c := ProviderConfig{ + c := upstreamldap.ProviderConfig{ Name: "original-provider-name", Host: testHost, CABundle: []byte("some-ca-bundle"), BindUsername: testBindUsername, BindPassword: testBindPassword, - UserSearch: UserSearchConfig{ + UserSearch: upstreamldap.UserSearchConfig{ Base: testUserSearchBase, Filter: testUserSearchFilter, UsernameAttribute: testUserSearchUsernameAttribute, @@ -1217,16 +1219,16 @@ func TestGetConfig(t *testing.T) { func TestGetURL(t *testing.T) { require.Equal(t, "ldaps://ldap.example.com:1234?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev", - New(ProviderConfig{ + New(upstreamldap.ProviderConfig{ Host: "ldap.example.com:1234", - UserSearch: UserSearchConfig{Base: "ou=users,dc=pinniped,dc=dev"}, + UserSearch: upstreamldap.UserSearchConfig{Base: "ou=users,dc=pinniped,dc=dev"}, }).GetURL().String()) require.Equal(t, "ldaps://ldap.example.com?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev", - New(ProviderConfig{ + New(upstreamldap.ProviderConfig{ Host: "ldap.example.com", - UserSearch: UserSearchConfig{Base: "ou=users,dc=pinniped,dc=dev"}, + UserSearch: upstreamldap.UserSearchConfig{Base: "ou=users,dc=pinniped,dc=dev"}, }).GetURL().String()) } @@ -1255,7 +1257,7 @@ func TestRealTLSDialing(t *testing.T) { tests := []struct { name string host string - connProto LDAPConnectionProtocol + connProto upstreamldap.LDAPConnectionProtocol caBundle []byte context context.Context wantError string @@ -1264,14 +1266,14 @@ func TestRealTLSDialing(t *testing.T) { name: "happy path", host: testServerHostAndPort, caBundle: []byte(testServerCABundle), - connProto: TLS, + connProto: upstreamldap.TLS, context: context.Background(), }, { name: "server cert name does not match the address to which the client connected", host: testServerWithBadCertNameAddr, caBundle: caForTestServerWithBadCertName.Bundle(), - connProto: TLS, + connProto: upstreamldap.TLS, context: context.Background(), wantError: `LDAP Result Code 200 "Network Error": x509: certificate is valid for 10.2.3.4, not 127.0.0.1`, }, @@ -1279,7 +1281,7 @@ func TestRealTLSDialing(t *testing.T) { name: "invalid CA bundle with TLS", host: testServerHostAndPort, caBundle: []byte("not a ca bundle"), - connProto: TLS, + connProto: upstreamldap.TLS, context: context.Background(), wantError: `LDAP Result Code 200 "Network Error": could not parse CA bundle`, }, @@ -1287,7 +1289,7 @@ func TestRealTLSDialing(t *testing.T) { name: "invalid CA bundle with StartTLS", host: testServerHostAndPort, caBundle: []byte("not a ca bundle"), - connProto: StartTLS, + connProto: upstreamldap.StartTLS, context: context.Background(), wantError: `LDAP Result Code 200 "Network Error": could not parse CA bundle`, }, @@ -1295,7 +1297,7 @@ func TestRealTLSDialing(t *testing.T) { name: "invalid host with TLS", host: "this:is:not:a:valid:hostname", caBundle: []byte(testServerCABundle), - connProto: TLS, + connProto: upstreamldap.TLS, context: context.Background(), wantError: `LDAP Result Code 200 "Network Error": host "this:is:not:a:valid:hostname" is not a valid hostname or IP address`, }, @@ -1303,7 +1305,7 @@ func TestRealTLSDialing(t *testing.T) { name: "invalid host with StartTLS", host: "this:is:not:a:valid:hostname", caBundle: []byte(testServerCABundle), - connProto: StartTLS, + connProto: upstreamldap.StartTLS, context: context.Background(), wantError: `LDAP Result Code 200 "Network Error": host "this:is:not:a:valid:hostname" is not a valid hostname or IP address`, }, @@ -1311,7 +1313,7 @@ func TestRealTLSDialing(t *testing.T) { name: "missing CA bundle when it is required because the host is not using a trusted CA", host: testServerHostAndPort, caBundle: nil, - connProto: TLS, + connProto: upstreamldap.TLS, context: context.Background(), wantError: `LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, }, @@ -1320,7 +1322,7 @@ func TestRealTLSDialing(t *testing.T) { // 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), - connProto: TLS, + connProto: upstreamldap.TLS, context: context.Background(), wantError: fmt.Sprintf(`LDAP Result Code 200 "Network Error": dial tcp %s: connect: connection refused`, recentlyClaimedHostAndPort), }, @@ -1328,7 +1330,7 @@ func TestRealTLSDialing(t *testing.T) { name: "pays attention to the passed context", host: testServerHostAndPort, caBundle: []byte(testServerCABundle), - connProto: TLS, + connProto: upstreamldap.TLS, context: alreadyCancelledContext, wantError: fmt.Sprintf(`LDAP Result Code 200 "Network Error": dial tcp %s: operation was canceled`, testServerHostAndPort), }, @@ -1344,7 +1346,7 @@ func TestRealTLSDialing(t *testing.T) { for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { - provider := New(ProviderConfig{ + provider := New(upstreamldap.ProviderConfig{ Host: tt.host, CABundle: tt.caBundle, ConnectionProtocol: tt.connProto, From 8fb35c65699fe22c9c019f1c37e7641d80f3def9 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 6 Jul 2021 11:34:54 -0700 Subject: [PATCH 03/36] Active Directory cli options --- .../types_activedirectoryidentityprovider.go.tmpl | 5 +++-- cmd/pinniped/cmd/kubeconfig.go | 2 +- cmd/pinniped/cmd/kubeconfig_test.go | 2 +- cmd/pinniped/cmd/login_oidc.go | 6 ++++-- cmd/pinniped/cmd/login_oidc_test.go | 15 +++++++++++++-- ...iped.dev_activedirectoryidentityproviders.yaml | 7 ++++--- generated/1.17/README.adoc | 2 +- .../types_activedirectoryidentityprovider.go | 5 +++-- ...iped.dev_activedirectoryidentityproviders.yaml | 7 ++++--- generated/1.18/README.adoc | 2 +- .../types_activedirectoryidentityprovider.go | 5 +++-- ...iped.dev_activedirectoryidentityproviders.yaml | 7 ++++--- generated/1.19/README.adoc | 2 +- .../types_activedirectoryidentityprovider.go | 5 +++-- ...iped.dev_activedirectoryidentityproviders.yaml | 7 ++++--- generated/1.20/README.adoc | 2 +- .../types_activedirectoryidentityprovider.go | 5 +++-- ...iped.dev_activedirectoryidentityproviders.yaml | 7 ++++--- .../types_activedirectoryidentityprovider.go | 5 +++-- internal/upstreamad/upstreamad.go | 14 +------------- internal/upstreamad/upstreamad_test.go | 2 +- pkg/oidcclient/login.go | 3 ++- test/integration/supervisor_login_test.go | 3 +++ 23 files changed, 68 insertions(+), 52 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl index 18726e7bd..dc108466c 100644 --- a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl @@ -72,8 +72,9 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is - // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // Optional, when not specified it will search the whole directory tree. + // Note that if your bind user only has permission to search a subtree, this must be specified. + // Search a subtree will also be faster. // +optional Base string `json:"base,omitempty"` diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 54042b348..7eb700313 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -153,7 +153,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { 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.oidc.upstreamIDPType, "upstream-identity-provider-type", "", "The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory')") 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)") diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index cb9f7b837..464271b38 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -150,7 +150,7 @@ func TestGetKubeconfig(t *testing.T) { --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') + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory') `) }, }, diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 9141d26ae..c1d2c321b 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -107,7 +107,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { 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')") + 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', 'activedirectory')") // --skip-listen is mainly needed for testing. We'll leave it hidden until we have a non-testing use case. mustMarkHidden(cmd, "skip-listen") @@ -165,10 +165,12 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin // this is the default, so don't need to do anything case "ldap": opts = append(opts, oidcclient.WithCLISendingCredentials()) + case "activedirectory": + 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( - "--upstream-identity-provider-type value not recognized: %s (supported values: oidc, ldap)", + "--upstream-identity-provider-type value not recognized: %s (supported values: oidc, ldap, activedirectory)", flags.upstreamIdentityProviderType) } diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 055dcec68..e888599eb 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -78,7 +78,7 @@ func TestLoginOIDCCommand(t *testing.T) { --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") + --upstream-identity-provider-type string The type of the upstream identity provider used during login with a Supervisor (e.g. 'oidc', 'ldap', 'activedirectory') (default "oidc") `), }, { @@ -148,7 +148,7 @@ func TestLoginOIDCCommand(t *testing.T) { }, wantError: true, wantStderr: here.Doc(` - Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap) + Error: --upstream-identity-provider-type value not recognized: invalid (supported values: oidc, ldap, activedirectory) `), }, { @@ -173,6 +173,17 @@ func TestLoginOIDCCommand(t *testing.T) { wantOptionsCount: 5, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", }, + { + name: "activedirectory upstream type is allowed", + args: []string{ + "--issuer", "test-issuer", + "--client-id", "test-client-id", + "--upstream-identity-provider-type", "activedirectory", + "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution + }, + wantOptionsCount: 5, + 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{ diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 03fbcd085..0f94c50a1 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -155,9 +155,10 @@ spec: base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will be defaulted based on the - host, for example if your active directory host is "activedirectory.example.com:636", - it will be "dc=activedirectory,dc=example,dc=com". + Optional, when not specified it will search the whole directory + tree. Note that if your bind user only has permission to search + a subtree, this must be specified. Search a subtree will also + be faster. type: string filter: description: Filter is the ActiveDirectory search filter which diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index d0ba80df3..288c802d9 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -875,7 +875,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be defaulted based on the host, for example if your active directory host is "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will search the whole directory tree. Note that if your bind user only has permission to search a subtree, this must be specified. Search a subtree will also be faster. | *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 18726e7bd..dc108466c 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -72,8 +72,9 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is - // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // Optional, when not specified it will search the whole directory tree. + // Note that if your bind user only has permission to search a subtree, this must be specified. + // Search a subtree will also be faster. // +optional Base string `json:"base,omitempty"` diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 03fbcd085..0f94c50a1 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -155,9 +155,10 @@ spec: base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will be defaulted based on the - host, for example if your active directory host is "activedirectory.example.com:636", - it will be "dc=activedirectory,dc=example,dc=com". + Optional, when not specified it will search the whole directory + tree. Note that if your bind user only has permission to search + a subtree, this must be specified. Search a subtree will also + be faster. type: string filter: description: Filter is the ActiveDirectory search filter which diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 12946f740..8d99ecc48 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -875,7 +875,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be defaulted based on the host, for example if your active directory host is "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will search the whole directory tree. Note that if your bind user only has permission to search a subtree, this must be specified. Search a subtree will also be faster. | *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 18726e7bd..dc108466c 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -72,8 +72,9 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is - // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // Optional, when not specified it will search the whole directory tree. + // Note that if your bind user only has permission to search a subtree, this must be specified. + // Search a subtree will also be faster. // +optional Base string `json:"base,omitempty"` diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 03fbcd085..0f94c50a1 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -155,9 +155,10 @@ spec: base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will be defaulted based on the - host, for example if your active directory host is "activedirectory.example.com:636", - it will be "dc=activedirectory,dc=example,dc=com". + Optional, when not specified it will search the whole directory + tree. Note that if your bind user only has permission to search + a subtree, this must be specified. Search a subtree will also + be faster. type: string filter: description: Filter is the ActiveDirectory search filter which diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index eadb1b33f..e999bf693 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -875,7 +875,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be defaulted based on the host, for example if your active directory host is "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will search the whole directory tree. Note that if your bind user only has permission to search a subtree, this must be specified. Search a subtree will also be faster. | *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 18726e7bd..dc108466c 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -72,8 +72,9 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is - // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // Optional, when not specified it will search the whole directory tree. + // Note that if your bind user only has permission to search a subtree, this must be specified. + // Search a subtree will also be faster. // +optional Base string `json:"base,omitempty"` diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 03fbcd085..0f94c50a1 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -155,9 +155,10 @@ spec: base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will be defaulted based on the - host, for example if your active directory host is "activedirectory.example.com:636", - it will be "dc=activedirectory,dc=example,dc=com". + Optional, when not specified it will search the whole directory + tree. Note that if your bind user only has permission to search + a subtree, this must be specified. Search a subtree will also + be faster. type: string filter: description: Filter is the ActiveDirectory search filter which diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 130514528..2eccb623a 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -875,7 +875,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be defaulted based on the host, for example if your active directory host is "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will search the whole directory tree. Note that if your bind user only has permission to search a subtree, this must be specified. Search a subtree will also be faster. | *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 18726e7bd..dc108466c 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -72,8 +72,9 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is - // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // Optional, when not specified it will search the whole directory tree. + // Note that if your bind user only has permission to search a subtree, this must be specified. + // Search a subtree will also be faster. // +optional Base string `json:"base,omitempty"` diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 03fbcd085..0f94c50a1 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -155,9 +155,10 @@ spec: base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will be defaulted based on the - host, for example if your active directory host is "activedirectory.example.com:636", - it will be "dc=activedirectory,dc=example,dc=com". + Optional, when not specified it will search the whole directory + tree. Note that if your bind user only has permission to search + a subtree, this must be specified. Search a subtree will also + be faster. type: string filter: description: Filter is the ActiveDirectory search filter which diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 18726e7bd..dc108466c 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -72,8 +72,9 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be defaulted based on the host, for example if your active directory host is - // "activedirectory.example.com:636", it will be "dc=activedirectory,dc=example,dc=com". + // Optional, when not specified it will search the whole directory tree. + // Note that if your bind user only has permission to search a subtree, this must be specified. + // Search a subtree will also be faster. // +optional Base string `json:"base,omitempty"` diff --git a/internal/upstreamad/upstreamad.go b/internal/upstreamad/upstreamad.go index 37fe3912b..c64be5bbe 100644 --- a/internal/upstreamad/upstreamad.go +++ b/internal/upstreamad/upstreamad.go @@ -417,19 +417,7 @@ func (p *Provider) userSearchRequest(username string) *ldap.SearchRequest { func (p *Provider) userSearchBase() string { if len(p.c.UserSearch.Base) == 0 { - parsed, err := endpointaddr.Parse(p.c.Host, 636) - if err != nil { - return "" - } - dcParts := strings.Split(parsed.Host, ".") - base := "" - for i, dcPart := range dcParts { - base += "dc=" + dcPart - if i < len(dcParts)-1 { - base += "," - } - } - return base + return "" } return p.c.UserSearch.Base } diff --git a/internal/upstreamad/upstreamad_test.go b/internal/upstreamad/upstreamad_test.go index 1fe0fca82..9c0c2a0bd 100644 --- a/internal/upstreamad/upstreamad_test.go +++ b/internal/upstreamad/upstreamad_test.go @@ -218,7 +218,7 @@ func TestEndUserAuthentication(t *testing.T) { conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { r.Filter = "(" + sAMAccountNameAttributeName + "=" + testUpstreamUsername + ")" r.Attributes = []string{sAMAccountNameAttributeName, testUserSearchUIDAttribute} - r.BaseDN = "dc=activedirectory,dc=example,dc=com" + r.BaseDN = "" })).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { diff --git a/pkg/oidcclient/login.go b/pkg/oidcclient/login.go index ffd827e91..b036eb142 100644 --- a/pkg/oidcclient/login.go +++ b/pkg/oidcclient/login.go @@ -228,7 +228,8 @@ func WithRequestAudience(audience string) Option { // 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 type supports this style of authentication. Currently this is supported by LDAPIdentityProviders. +// provider type supports this style of authentication. Currently this is supported by LDAPIdentityProviders and +// ActiveDirectoryIdentityProviders. // 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 { diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index b747beafa..e1d1049f0 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -230,6 +230,9 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN), wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs, }, + { + name: "activedirectory with all default options", + }, } for _, test := range tests { tt := test From 3b8edb84a58046df4dbd478edb7a166d5c5be348 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 7 Jul 2021 09:23:32 -0700 Subject: [PATCH 04/36] WIP on active directory integration test --- test/integration/e2e_test.go | 4 +- test/integration/supervisor_login_test.go | 80 ++++++++++++++++++++++- test/testlib/client.go | 41 ++++++++++++ test/testlib/env.go | 27 ++++++-- 4 files changed, 140 insertions(+), 12 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index fd631822b..1eb8f724c 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -386,7 +386,7 @@ func TestE2EFullIntegration(t *testing.T) { t.Skip("LDAP integration test requires connectivity to an LDAP server") } - expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue + expectedUsername := env.SupervisorUpstreamLDAP.TestUsernameAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. @@ -422,7 +422,7 @@ func TestE2EFullIntegration(t *testing.T) { Base: env.SupervisorUpstreamLDAP.UserSearchBase, Filter: "", Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + Username: env.SupervisorUpstreamLDAP.TestUsernameAttributeName, UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index e1d1049f0..1303f8d60 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -128,7 +128,7 @@ func TestSupervisorLogin(t *testing.T) { Base: env.SupervisorUpstreamLDAP.UserSearchBase, Filter: "", Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + Username: env.SupervisorUpstreamLDAP.TestUsernameAttributeName, UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, @@ -150,7 +150,7 @@ func TestSupervisorLogin(t *testing.T) { 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.TestUsernameAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login httpClient, ) @@ -162,7 +162,7 @@ func TestSupervisorLogin(t *testing.T) { "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUsernameAttributeValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { @@ -232,6 +232,56 @@ func TestSupervisorLogin(t *testing.T) { }, { name: "activedirectory with all default options", + maybeSkip: func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + if env.SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } + }, + createIDP: func(t *testing.T) { + t.Helper() + secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, + }, + ) + adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), + }, + Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ + SecretName: secret.Name, + }, + }, idpv1alpha1.ActiveDirectoryPhaseReady) + expectedMsg := fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) // TODO refactor to be same as LDAP func + }, + requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { + requestAuthorizationUsingLDAPIdentityProvider(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamActiveDirectory.TestUsernameAttributeName, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.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.SupervisorUpstreamActiveDirectory.Host + + "?base=" + url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase) + + "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue)), + ), + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserDN), + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs, }, } for _, test := range tests { @@ -274,6 +324,30 @@ func requireSuccessfulLDAPIdentityProviderConditions(t *testing.T, ldapIDP *idpv {"LDAPConnectionValid", "True", "Success"}, }, conditionsSummary) } +func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider, expectedActiveDirectoryConnectionValidMessage string) { + require.Len(t, adIDP.Status.Conditions, 3) + + conditionsSummary := [][]string{} + for _, condition := range adIDP.Status.Conditions { + conditionsSummary = append(conditionsSummary, []string{condition.Type, string(condition.Status), condition.Reason}) + t.Logf("Saw ActiveDirectoryIdentityProvider 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, expectedActiveDirectoryConnectionValidMessage, condition.Message) + } + } + + require.ElementsMatch(t, [][]string{ + {"BindSecretValid", "True", "Success"}, + {"TLSConfigurationValid", "True", "Success"}, + {"LDAPConnectionValid", "True", "Success"}, + }, conditionsSummary) +} func testSupervisorLogin( t *testing.T, diff --git a/test/testlib/client.go b/test/testlib/client.go index 71de190a0..a206c2d5e 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -434,6 +434,47 @@ func CreateTestLDAPIdentityProvider(t *testing.T, spec idpv1alpha1.LDAPIdentityP return result } +func CreateTestActiveDirectoryIdentityProvider(t *testing.T, spec idpv1alpha1.ActiveDirectoryIdentityProviderSpec, expectedPhase idpv1alpha1.ActiveDirectoryIdentityProviderPhase) *idpv1alpha1.ActiveDirectoryIdentityProvider { + 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().ActiveDirectoryIdentityProviders(env.SupervisorNamespace) + + created, err := upstreams.Create(ctx, &idpv1alpha1.ActiveDirectoryIdentityProvider{ + ObjectMeta: testObjectMeta(t, "upstream-ad-idp"), + Spec: spec, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + // Always clean this up after this point. + t.Cleanup(func() { + t.Logf("cleaning up test ActiveDirectoryIdentityProvider %s/%s", created.Namespace, created.Name) + err := upstreams.Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + t.Logf("created test ActiveDirectoryIdentityProvider %s", created.Name) + + // Wait for the LDAPIdentityProvider to enter the expected phase (or time out). + var result *idpv1alpha1.ActiveDirectoryIdentityProvider + RequireEventuallyf(t, + func(requireEventually *require.Assertions) { + var err error + result, err = upstreams.Get(ctx, created.Name, metav1.GetOptions{}) + requireEventually.NoErrorf(err, "error while getting ActiveDirectoryIdentityProvider %s/%s", created.Namespace, created.Name) + requireEventually.Equalf(expectedPhase, result.Status.Phase, "ActiveDirectoryIdentityProvider is not in phase %s: %v", expectedPhase, Sdump(result)) + }, + 2*time.Minute, // it takes 1 minute for a failed LDAP TLS connection test to timeout before it tries using StartTLS, so wait longer than that + 1*time.Second, + "expected the ActiveDirectoryIdentityProvider 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) diff --git a/test/testlib/env.go b/test/testlib/env.go index ea6834b6a..c5afa9e84 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -61,9 +61,10 @@ type TestEnv struct { ExpectedGroups []string `json:"expectedGroups"` } `json:"testUser"` - CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"` - SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"` - SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"` + CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"` + SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"` + SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"` + SupervisorUpstreamActiveDirectory TestLDAPUpstream `json:"supervisorActiveDirectoryUpstream"` } type TestOIDCUpstream struct { @@ -91,8 +92,8 @@ type TestLDAPUpstream struct { TestUserDN string `json:"testUserDN"` TestUserCN string `json:"testUserCN"` TestUserPassword string `json:"testUserPassword"` - TestUserMailAttributeName string `json:"testUserMailAttributeName"` - TestUserMailAttributeValue string `json:"testUserMailAttributeValue"` + TestUsernameAttributeName string `json:"testUserMailAttributeName"` + TestUsernameAttributeValue string `json:"testUserMailAttributeValue"` TestUserUniqueIDAttributeName string `json:"testUserUniqueIDAttributeName"` TestUserUniqueIDAttributeValue string `json:"testUserUniqueIDAttributeValue"` TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` @@ -260,13 +261,25 @@ func loadEnvVars(t *testing.T, result *TestEnv) { 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"), + TestUsernameAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME"), + TestUsernameAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE"), TestUserDirectGroupsCNs: filterEmpty(strings.Split(needEnv(t, "PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_CN"), ";")), TestUserDirectGroupsDNs: filterEmpty(strings.Split(needEnv(t, "PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_DN"), ";")), TestUserPassword: needEnv(t, "PINNIPED_TEST_LDAP_USER_PASSWORD"), } + result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{ + Host: wantEnv("PINNIPED_TEST_AD_HOST", ""), + CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")), + BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""), + BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""), + TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), + TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), + TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), + TestUsernameAttributeName: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_NAME", ""), + TestUsernameAttributeValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + } + sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) } From be6f9f83ce854d223ae126e61b769726a8bab484 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 7 Jul 2021 11:15:52 -0700 Subject: [PATCH 05/36] RBAC rules for activedirectoryidentityprovider --- deploy/supervisor/rbac.yaml | 8 ++++++++ .../active_directory_upstream_watcher.go | 2 +- test/integration/kube_api_discovery_test.go | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/deploy/supervisor/rbac.yaml b/deploy/supervisor/rbac.yaml index 60447f7c5..65530dc48 100644 --- a/deploy/supervisor/rbac.yaml +++ b/deploy/supervisor/rbac.yaml @@ -40,6 +40,14 @@ rules: - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") resources: [ldapidentityproviders/status] verbs: [get, patch, update] + - apiGroups: + - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + resources: [activedirectoryidentityproviders] + verbs: [get, list, watch] + - apiGroups: + - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + resources: [activedirectoryidentityproviders/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: [""] diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 5936c6bd8..39e5a0b38 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -1,7 +1,7 @@ // Copyright 2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -// Package activedirectoryupstreamwatcher implements a controller which watches LDAPIdentityProviders. +// Package activedirectoryupstreamwatcher implements a controller which watches ActiveDirectoryIdentityProviders. package activedirectoryupstreamwatcher import ( diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go index 9632ee70f..2e73cd78f 100644 --- a/test/integration/kube_api_discovery_test.go +++ b/test/integration/kube_api_discovery_test.go @@ -184,6 +184,20 @@ func TestGetAPIResourceList(t *testing.T) { Kind: "LDAPIdentityProvider", Verbs: []string{"get", "patch", "update"}, }, + { + Name: "activedirectoryidentityproviders", + SingularName: "activedirectoryidentityprovider", + Namespaced: true, + Kind: "ActiveDirectoryIdentityProvider", + Verbs: []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"}, + Categories: []string{"pinniped", "pinniped-idp", "pinniped-idps"}, + }, + { + Name: "activedirectoryidentityproviders/status", + Namespaced: true, + Kind: "ActiveDirectoryIdentityProvider", + Verbs: []string{"get", "patch", "update"}, + }, }, }, }, From 94e90a5d2611734b821eb812d52373c47ea90c4d Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 7 Jul 2021 14:52:13 -0700 Subject: [PATCH 06/36] groups related env variables for AD --- test/testlib/env.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/testlib/env.go b/test/testlib/env.go index c5afa9e84..5d68d4a64 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -278,6 +278,8 @@ func loadEnvVars(t *testing.T, result *TestEnv) { TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), TestUsernameAttributeName: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_NAME", ""), TestUsernameAttributeValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), + TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), } sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) From 5c283d941cc70f93eb4b70f4e392c55233363edf Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 8 Jul 2021 15:00:04 -0700 Subject: [PATCH 07/36] Helper script for running active directory tests --- hack/prepare-for-integration-tests.sh | 23 +++++++++++++++++++++++ test/integration/supervisor_login_test.go | 17 ++++++++--------- test/testlib/client.go | 5 +++-- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index b918049bd..ea57a557f 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -50,6 +50,7 @@ skip_build=no clean_kind=no api_group_suffix="pinniped.dev" # same default as in the values.yaml ytt file skip_chromedriver_check=no +test_active_directory=no while (("$#")); do case "$1" in @@ -79,6 +80,10 @@ while (("$#")); do skip_chromedriver_check=yes shift ;; + --test-active-directory) + test_active_directory=yes + shift + ;; -*) log_error "Unsupported flag $1" >&2 exit 1 @@ -369,6 +374,24 @@ export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD=${dex_test_password} export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_EXPECTED_GROUPS= # Dex's local user store does not let us configure groups. export PINNIPED_TEST_API_GROUP_SUFFIX='${api_group_suffix}' +if [[ "$test_active_directory" == "yes" ]]; then + +if [[ -z "$(gcloud config list account --format "value(core.account)")" ]]; then + echo "Please run \`gcloud auth login\`" + exit 1 +fi + +export PINNIPED_TEST_AD_HOST="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-host' -))" +export PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-bind-account-username' -))" +export PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-bind-account-password' -)" +export PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME="objectGUID" +export PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-unique-id-attribute-value' -)" +export PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_NAME="sAMAccountName" +export PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-sAMAccountName' -))" +export PINNIPED_TEST_AD_USER_PASSWORD="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-password' -)" +export PINNIPED_TEST_AD_LDAPS_CA_BUNDLE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-ca-data' -))" +fi + read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true ${pinniped_cluster_capability_file_content} PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 1303f8d60..aec980948 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -250,7 +250,7 @@ func TestSupervisorLogin(t *testing.T) { }, ) adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ - Host: env.SupervisorUpstreamLDAP.Host, + Host: env.SupervisorUpstreamActiveDirectory.Host, TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), }, @@ -268,19 +268,18 @@ func TestSupervisorLogin(t *testing.T) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorizationUsingLDAPIdentityProvider(t, downstreamAuthorizeURL, - env.SupervisorUpstreamActiveDirectory.TestUsernameAttributeName, // username to present to server during login - env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUsernameAttributeValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.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.SupervisorUpstreamActiveDirectory.Host + - "?base=" + url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase) + "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue)), ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserDN), + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUsernameAttributeValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs, }, } @@ -337,7 +336,7 @@ func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, ad require.Equal(t, "loaded bind secret", condition.Message) case "TLSConfigurationValid": require.Equal(t, "loaded TLS configuration", condition.Message) - case "LDAPConnectionValid": + case "ActiveDirectoryConnectionValid": require.Equal(t, expectedActiveDirectoryConnectionValidMessage, condition.Message) } } @@ -345,7 +344,7 @@ func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, ad require.ElementsMatch(t, [][]string{ {"BindSecretValid", "True", "Success"}, {"TLSConfigurationValid", "True", "Success"}, - {"LDAPConnectionValid", "True", "Success"}, + {"ActiveDirectoryConnectionValid", "True", "Success"}, }, conditionsSummary) } @@ -604,7 +603,7 @@ func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAutho func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAuthorizeURL, upstreamUsername, upstreamPassword string, httpClient *http.Client) { t.Helper() - ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) + ctx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Minute) defer cancelFunc() authRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) @@ -635,7 +634,7 @@ func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAutho return false, nil } return true, nil - }, 30*time.Second, 200*time.Millisecond) + }, 60*time.Second, 200*time.Millisecond) expectSecurityHeaders(t, authResponse, true) diff --git a/test/testlib/client.go b/test/testlib/client.go index a206c2d5e..0cebe5ada 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -15,11 +15,12 @@ import ( "testing" "time" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/stretchr/testify/require" authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" @@ -441,7 +442,7 @@ func CreateTestActiveDirectoryIdentityProvider(t *testing.T, spec idpv1alpha1.Ac ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - // Create the LDAPIdentityProvider using GenerateName to get a random name. + // Create the ActiveDirectoryIdentityProvider using GenerateName to get a random name. upstreams := client.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace) created, err := upstreams.Create(ctx, &idpv1alpha1.ActiveDirectoryIdentityProvider{ From b3d0b28bd0fd70a0ec3711a759641efbb9fd1c58 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Mon, 12 Jul 2021 13:29:37 -0700 Subject: [PATCH 08/36] Integration test fixes, fixing objectGUID handling --- hack/prepare-for-integration-tests.sh | 10 ++++++---- internal/oidc/auth/auth_handler_test.go | 23 +++++++++++++++++++++++ internal/upstreamad/upstreamad.go | 10 +--------- internal/upstreamad/upstreamad_test.go | 3 ++- test/integration/supervisor_login_test.go | 11 +++++++++-- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index ea57a557f..bdc9ec425 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -381,15 +381,17 @@ if [[ -z "$(gcloud config list account --format "value(core.account)")" ]]; then exit 1 fi -export PINNIPED_TEST_AD_HOST="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-host' -))" -export PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-bind-account-username' -))" +export PINNIPED_TEST_AD_HOST="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-host' -)" +export PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-bind-account-username' -)" export PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-bind-account-password' -)" export PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME="objectGUID" export PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-unique-id-attribute-value' -)" export PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_NAME="sAMAccountName" -export PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-sAMAccountName' -))" +export PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-sAMAccountName' -)" export PINNIPED_TEST_AD_USER_PASSWORD="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-password' -)" -export PINNIPED_TEST_AD_LDAPS_CA_BUNDLE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-ca-data' -))" +export PINNIPED_TEST_AD_LDAPS_CA_BUNDLE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-ca-data' -)" +export PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-expected-direct-groups-dn' -)" +export PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-expected-direct-groups-cn' -)" fi read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 9a4a99efc..e12c10044 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -477,6 +477,29 @@ func TestAuthorizationEndpoint(t *testing.T) { wantDownstreamPKCEChallenge: downstreamPKCEChallenge, wantDownstreamPKCEChallengeMethod: downstreamPKCEChallengeMethod, }, + { + name: "Active Directory upstream happy path using POST", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodPost, + path: "/some/path", + contentType: "application/x-www-form-urlencoded", + body: encodeQuery(happyGetRequestQueryMap), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.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(), diff --git a/internal/upstreamad/upstreamad.go b/internal/upstreamad/upstreamad.go index c64be5bbe..c6e457b84 100644 --- a/internal/upstreamad/upstreamad.go +++ b/internal/upstreamad/upstreamad.go @@ -369,8 +369,6 @@ func (p *Provider) searchAndBindUser(conn upstreamldap.Conn, username string, bi return "", "", nil, err } - // We would like to support binary typed attributes for UIDs, so always read them as binary and encode them, - // even when the attribute may not be binary. mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.uidAttribute(), userEntry, username) if err != nil { return "", "", nil, err @@ -523,13 +521,7 @@ func (p *Provider) getSearchResultAttributeRawValueEncoded(attributeName string, } if attributeName == objectGUIDAttributeName { - // In AD, objectGUID will be represented as a base64-encoded UUID. Convert it back to UUID encoding. - base64decoded, err := base64.StdEncoding.DecodeString(entry.GetAttributeValue(attributeName)) - if err != nil { - // TODO if there is an error, should we throw it or pass it through as base64? - return "", fmt.Errorf("Error decoding UID: %s", err.Error()) - } - uuidEntry, err := uuid.FromBytes(base64decoded) + uuidEntry, err := uuid.FromBytes(attributeValue) if err != nil { return "", fmt.Errorf("Error decoding UID: %s", err.Error()) } diff --git a/internal/upstreamad/upstreamad_test.go b/internal/upstreamad/upstreamad_test.go index 9c0c2a0bd..346741b53 100644 --- a/internal/upstreamad/upstreamad_test.go +++ b/internal/upstreamad/upstreamad_test.go @@ -46,7 +46,7 @@ const ( testGroupSearchResultDNValue1 = "some-upstream-group-dn1" testGroupSearchResultDNValue2 = "some-upstream-group-dn2" testUserSearchResultUsernameAttributeValue = "some-upstream-username-value" - testUserSearchResultUIDAttributeValue = "Ej5FZ+ibEtOkVkJmFBdAAA==" // this is base64 encoded 123e4567-e89b-12d3-a456-426614174000 + testUserSearchResultUIDAttributeValue = "\x12>Eg\xe8\x9b\x12\u04e4VBf\x14\x17@\x00" // binary representation of 123e4567-e89b-12d3-a456-426614174000 testGroupSearchResultGroupNameAttributeValue1 = "some-upstream-group-name-value1" testGroupSearchResultGroupNameAttributeValue2 = "some-upstream-group-name-value2" @@ -207,6 +207,7 @@ func TestEndUserAuthentication(t *testing.T) { ConnectionProtocol: upstreamldap.TLS, BindUsername: testBindUsername, BindPassword: testBindPassword, + // no user search... that's all defaulted. GroupSearch: upstreamldap.GroupSearchConfig{ Base: testGroupSearchBase, Filter: testGroupSearchFilter, diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index aec980948..df53f302b 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -254,6 +254,12 @@ func TestSupervisorLogin(t *testing.T) { TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), }, + UserSearch: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearch{ + Base: "dc=activedirectory,dc=test,dc=pinniped,dc=dev", + }, + GroupSearch: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearch{ + Base: "dc=activedirectory,dc=test,dc=pinniped,dc=dev", + }, Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ SecretName: secret.Name, }, @@ -276,11 +282,12 @@ func TestSupervisorLogin(t *testing.T) { // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( "ldaps://" + env.SupervisorUpstreamActiveDirectory.Host + - "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue)), + "?base=" + url.QueryEscape("dc=activedirectory,dc=test,dc=pinniped,dc=dev") + + "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUsernameAttributeValue), - wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs, }, } for _, test := range tests { From aaa486137387162422cd38259cbbf3044075c57b Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 15 Jul 2021 11:32:15 -0700 Subject: [PATCH 09/36] Custom API Group overlay for AD Signed-off-by: Margo Crawford --- cmd/pinniped-supervisor/main.go | 3 +-- deploy/supervisor/z0_crd_overlay.yaml | 9 +++++++++ .../active_directory_upstream_watcher.go | 5 ++--- .../active_directory_upstream_watcher_test.go | 6 +++--- internal/upstreamad/upstreamad.go | 3 +-- internal/upstreamad/upstreamad_test.go | 3 +-- test/integration/supervisor_login_test.go | 4 ++-- test/testlib/client.go | 3 +-- test/testlib/env.go | 2 ++ 9 files changed, 22 insertions(+), 16 deletions(-) diff --git a/cmd/pinniped-supervisor/main.go b/cmd/pinniped-supervisor/main.go index 3ac3ad1e6..fe719af86 100644 --- a/cmd/pinniped-supervisor/main.go +++ b/cmd/pinniped-supervisor/main.go @@ -15,8 +15,6 @@ import ( "strings" "time" - "go.pinniped.dev/internal/controller/supervisorconfig/activedirectoryupstreamwatcher" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/clock" @@ -33,6 +31,7 @@ import ( pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" "go.pinniped.dev/internal/config/supervisor" "go.pinniped.dev/internal/controller/supervisorconfig" + "go.pinniped.dev/internal/controller/supervisorconfig/activedirectoryupstreamwatcher" "go.pinniped.dev/internal/controller/supervisorconfig/generator" "go.pinniped.dev/internal/controller/supervisorconfig/ldapupstreamwatcher" "go.pinniped.dev/internal/controller/supervisorconfig/oidcupstreamwatcher" diff --git a/deploy/supervisor/z0_crd_overlay.yaml b/deploy/supervisor/z0_crd_overlay.yaml index 8e2dca111..7596975d4 100644 --- a/deploy/supervisor/z0_crd_overlay.yaml +++ b/deploy/supervisor/z0_crd_overlay.yaml @@ -31,3 +31,12 @@ metadata: name: #@ pinnipedDevAPIGroupWithPrefix("ldapidentityproviders.idp.supervisor") spec: group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") + +#@overlay/match by=overlay.subset({"kind": "CustomResourceDefinition", "metadata":{"name":"activedirectoryidentityproviders.idp.supervisor.pinniped.dev"}}), expects=1 +--- +metadata: + #@overlay/match missing_ok=True + labels: #@ labels() + name: #@ pinnipedDevAPIGroupWithPrefix("activedirectoryidentityproviders.idp.supervisor") +spec: + group: #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 39e5a0b38..3d6117103 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -11,8 +11,6 @@ import ( "fmt" "time" - "go.pinniped.dev/internal/upstreamad" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,6 +27,7 @@ import ( "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/upstreamad" "go.pinniped.dev/internal/upstreamldap" ) @@ -133,7 +132,7 @@ func newInternal( func (c *activeDirectoryWatcherController) Sync(ctx controllerlib.Context) error { actualUpstreams, err := c.activeDirectoryIdentityProviderInformer.Lister().List(labels.Everything()) if err != nil { - return fmt.Errorf("failed to list LDAPIdentityProviders: %w", err) + return fmt.Errorf("failed to list ActiveDirectoryIdentityProviders: %w", err) } requeue := false diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 1b7ae2aae..c14c43903 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -35,7 +35,7 @@ import ( "go.pinniped.dev/internal/upstreamldap" ) -func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { +func TestActiveDirectoryUpstreamWatcherControllerFilterSecrets(t *testing.T) { t.Parallel() tests := []struct { @@ -94,7 +94,7 @@ func TestLDAPUpstreamWatcherControllerFilterSecrets(t *testing.T) { } } -func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) { +func TestActiveDirectoryUpstreamWatcherControllerFilterActiveDirectoryIdentityProviders(t *testing.T) { t.Parallel() tests := []struct { @@ -105,7 +105,7 @@ func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders(t *testing.T) wantDelete bool }{ { - name: "any LDAPIdentityProvider", + name: "any ActiveDirectoryIdentityProvider", idp: &v1alpha1.ActiveDirectoryIdentityProvider{ ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, }, diff --git a/internal/upstreamad/upstreamad.go b/internal/upstreamad/upstreamad.go index c6e457b84..5b2f34fa3 100644 --- a/internal/upstreamad/upstreamad.go +++ b/internal/upstreamad/upstreamad.go @@ -17,8 +17,6 @@ import ( "strings" "time" - "go.pinniped.dev/internal/upstreamldap" - "github.com/go-ldap/ldap/v3" "github.com/gofrs/uuid" "k8s.io/apiserver/pkg/authentication/authenticator" @@ -29,6 +27,7 @@ import ( "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/upstreamldap" ) const ( diff --git a/internal/upstreamad/upstreamad_test.go b/internal/upstreamad/upstreamad_test.go index 346741b53..579bddd27 100644 --- a/internal/upstreamad/upstreamad_test.go +++ b/internal/upstreamad/upstreamad_test.go @@ -15,8 +15,6 @@ import ( "testing" "time" - "go.pinniped.dev/internal/upstreamldap" - "github.com/go-ldap/ldap/v3" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" @@ -27,6 +25,7 @@ import ( "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/mocks/mockldapconn" "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/upstreamldap" ) const ( diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index df53f302b..bfb938867 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -610,7 +610,7 @@ func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAutho func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAuthorizeURL, upstreamUsername, upstreamPassword string, httpClient *http.Client) { t.Helper() - ctx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Minute) + ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) defer cancelFunc() authRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, downstreamAuthorizeURL, nil) @@ -641,7 +641,7 @@ func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAutho return false, nil } return true, nil - }, 60*time.Second, 200*time.Millisecond) + }, 30*time.Second, 200*time.Millisecond) expectSecurityHeaders(t, authResponse, true) diff --git a/test/testlib/client.go b/test/testlib/client.go index 0cebe5ada..8c91f40a3 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -15,12 +15,11 @@ import ( "testing" "time" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - "github.com/stretchr/testify/require" authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" diff --git a/test/testlib/env.go b/test/testlib/env.go index 5d68d4a64..64706e589 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -284,6 +284,8 @@ func loadEnvVars(t *testing.T, result *TestEnv) { sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) + sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs) + sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs) } func (e *TestEnv) HasCapability(cap Capability) bool { From 7696f4256d2382d03d2cb4afa5876e1a1fd7f576 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 15 Jul 2021 16:33:42 -0700 Subject: [PATCH 10/36] Move defaulting of ad username and uid attributes to controller Now the controller uses upstreamldap so there is less duplication, since they are very similar. Signed-off-by: Ryan Richard --- .../active_directory_upstream_watcher.go | 22 +- .../active_directory_upstream_watcher_test.go | 49 +- internal/upstreamad/upstreamad.go | 567 ------- internal/upstreamad/upstreamad_test.go | 1377 ----------------- 4 files changed, 61 insertions(+), 1954 deletions(-) delete mode 100644 internal/upstreamad/upstreamad.go delete mode 100644 internal/upstreamad/upstreamad_test.go diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 3d6117103..0394bdada 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -27,7 +27,6 @@ import ( "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" "go.pinniped.dev/internal/plog" - "go.pinniped.dev/internal/upstreamad" "go.pinniped.dev/internal/upstreamldap" ) @@ -43,6 +42,10 @@ const ( reasonActiveDirectoryConnectionError = "ActiveDirectoryConnectionError" noTLSConfigurationMessage = "no TLS configuration provided" loadedTLSConfigurationMessage = "loaded TLS configuration" + + // Default values for active directory config + defaultActiveDirectoryUsernameAttributeName = "sAMAccountName" + defaultActiveDirectoryUIDAttributeName = "objectGUID" ) // UpstreamActiveDirectoryIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. @@ -158,14 +161,23 @@ func (c *activeDirectoryWatcherController) Sync(ctx controllerlib.Context) error func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) { spec := upstream.Spec + usernameAttribute := spec.UserSearch.Attributes.Username + if len(usernameAttribute) == 0 { + usernameAttribute = defaultActiveDirectoryUsernameAttributeName + } + uidAttribute := spec.UserSearch.Attributes.UID + if len(uidAttribute) == 0 { + uidAttribute = defaultActiveDirectoryUIDAttributeName + } + config := &upstreamldap.ProviderConfig{ Name: upstream.Name, Host: spec.Host, UserSearch: upstreamldap.UserSearchConfig{ Base: spec.UserSearch.Base, Filter: spec.UserSearch.Filter, - UsernameAttribute: spec.UserSearch.Attributes.Username, - UIDAttribute: spec.UserSearch.Attributes.UID, + UsernameAttribute: usernameAttribute, + UIDAttribute: uidAttribute, }, GroupSearch: upstreamldap.GroupSearchConfig{ Base: spec.GroupSearch.Base, @@ -198,12 +210,12 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, 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 = upstreamad.New(*config) + 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 = upstreamad.New(*config) + p = upstreamldap.New(*config) requeue = false } diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index c14c43903..95468c2a3 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -12,8 +12,6 @@ import ( "testing" "time" - "go.pinniped.dev/internal/upstreamad" - "github.com/go-ldap/ldap/v3" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" @@ -139,7 +137,7 @@ func TestActiveDirectoryUpstreamWatcherControllerFilterActiveDirectoryIdentityPr } } -// Wrap the func into a struct so the test can do deep equal assertions on instances of upstreamad.Provider. +// Wrap the func into a struct so the test can do deep equal assertions on instances of upstreamldap.Provider. type comparableDialer struct { upstreamldap.LDAPDialerFunc } @@ -850,6 +848,47 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }}, wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, + { + name: "when the input activedirectoryidentityprovider leaves user attributes blank, provide default values", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + 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, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: "sAMAccountName", + UIDAttribute: "objectGUID", + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + }, } for _, tt := range tests { @@ -863,7 +902,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { kubeInformers := informers.NewSharedInformerFactory(fakeKubeClient, 0) cache := provider.NewDynamicUpstreamIDPProvider() cache.SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ - upstreamad.New(upstreamldap.ProviderConfig{Name: "initial-entry"}), + upstreamldap.New(upstreamldap.ProviderConfig{Name: "initial-entry"}), }) ctrl := gomock.NewController(t) @@ -917,7 +956,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { actualIDPList := cache.GetActiveDirectoryIdentityProviders() require.Equal(t, len(tt.wantResultingCache), len(actualIDPList)) for i := range actualIDPList { - actualIDP := actualIDPList[i].(*upstreamad.Provider) + actualIDP := actualIDPList[i].(*upstreamldap.Provider) copyOfExpectedValueForResultingCache := *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. diff --git a/internal/upstreamad/upstreamad.go b/internal/upstreamad/upstreamad.go deleted file mode 100644 index 5b2f34fa3..000000000 --- a/internal/upstreamad/upstreamad.go +++ /dev/null @@ -1,567 +0,0 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Package upstreamad implements an active directory specific abstraction of upstream LDAP IDP interactions. -package upstreamad - -import ( - "context" - "crypto/tls" - "crypto/x509" - "encoding/base64" - "errors" - "fmt" - "net" - "net/url" - "sort" - "strings" - "time" - - "github.com/go-ldap/ldap/v3" - "github.com/gofrs/uuid" - "k8s.io/apiserver/pkg/authentication/authenticator" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/utils/trace" - - "go.pinniped.dev/internal/authenticators" - "go.pinniped.dev/internal/endpointaddr" - "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/plog" - "go.pinniped.dev/internal/upstreamldap" -) - -const ( - ldapsScheme = "ldaps" - distinguishedNameAttributeName = "dn" - objectGUIDAttributeName = "objectGUID" - sAMAccountNameAttributeName = "sAMAccountName" - searchFilterInterpolationLocationMarker = "{}" - groupSearchPageSize = uint32(250) - defaultLDAPPort = uint16(389) - defaultLDAPSPort = uint16(636) -) - -// UserSearchConfig contains information about how to search for users in the upstream active directory IDP. -type UserSearchConfig struct { - // Base is the base DN to use for the user search in the upstream active directory IDP. - Base string - - // Filter is the filter to use for the user search in the upstream active directory IDP. - Filter string - - // UsernameAttribute is the attribute in the LDAP entry from which the username should be - // retrieved. Empty means to use 'sAMAccountName'. - UsernameAttribute string - - // UIDAttribute is the attribute in the LDAP entry from which the user's unique ID should be - // retrieved. Empty means to use 'objectGUID'. - UIDAttribute string -} - -// GroupSearchConfig contains information about how to search for group membership for users in the upstream active directory IDP. -type GroupSearchConfig struct { - // Base is the base DN to use for the group search in the upstream active directory IDP. Empty means to skip group search - // entirely, in which case authenticated users will not belong to any groups from the upstream active directory IDP. - Base string - - // Filter is the filter to use for the group search in the upstream active directory IDP. Empty means to use `member={}`. - Filter string - - // GroupNameAttribute is the attribute in the LDAP group entry from which the group name should be - // retrieved. Empty means to use 'cn'. - GroupNameAttribute string -} - -type Provider struct { - c upstreamldap.ProviderConfig -} - -var _ provider.UpstreamLDAPIdentityProviderI = &Provider{} -var _ authenticators.UserAuthenticator = &Provider{} - -// 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 upstreamldap.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() upstreamldap.ProviderConfig { - return p.c -} - -func (p *Provider) dial(ctx context.Context) (upstreamldap.Conn, error) { - tlsAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPSPort) - if err != nil { - return nil, ldap.NewError(ldap.ErrorNetwork, err) - } - - startTLSAddr, err := endpointaddr.Parse(p.c.Host, defaultLDAPPort) - if err != nil { - return nil, ldap.NewError(ldap.ErrorNetwork, err) - } - - // Choose how and where to dial based on TLS vs. StartTLS config option. - var dialFunc upstreamldap.LDAPDialerFunc - var addr endpointaddr.HostPort - switch { - case p.c.ConnectionProtocol == upstreamldap.TLS: - dialFunc = p.dialTLS - addr = tlsAddr - case p.c.ConnectionProtocol == upstreamldap.StartTLS: - dialFunc = p.dialStartTLS - addr = startTLSAddr - default: - return nil, ldap.NewError(ldap.ErrorNetwork, fmt.Errorf("did not specify valid ConnectionProtocol")) - } - - // Override the real dialer for testing purposes sometimes. - if p.c.Dialer != nil { - dialFunc = p.c.Dialer.Dial - } - - return dialFunc(ctx, addr) -} - -// dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is TLS. -// 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, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { - tlsConfig, err := p.tlsConfig() - if err != nil { - return nil, ldap.NewError(ldap.ErrorNetwork, err) - } - - dialer := &tls.Dialer{NetDialer: netDialer(), Config: tlsConfig} - c, err := dialer.DialContext(ctx, "tcp", addr.Endpoint()) - if err != nil { - return nil, ldap.NewError(ldap.ErrorNetwork, err) - } - - conn := ldap.NewConn(c, true) - conn.Start() - return conn, nil -} - -// dialTLS is a default implementation of the Dialer, used when Dialer is nil and ConnectionProtocol is StartTLS. -// 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) dialStartTLS(ctx context.Context, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { - tlsConfig, err := p.tlsConfig() - if err != nil { - return nil, ldap.NewError(ldap.ErrorNetwork, err) - } - - // Unfortunately, this seems to be required for StartTLS, even though it is not needed for regular TLS. - tlsConfig.ServerName = addr.Host - - c, err := netDialer().DialContext(ctx, "tcp", addr.Endpoint()) - if err != nil { - return nil, ldap.NewError(ldap.ErrorNetwork, err) - } - - conn := ldap.NewConn(c, false) - conn.Start() - err = conn.StartTLS(tlsConfig) - if err != nil { - return nil, err - } - - return conn, nil -} - -func netDialer() *net.Dialer { - return &net.Dialer{Timeout: time.Minute} -} - -func (p *Provider) tlsConfig() (*tls.Config, error) { - var rootCAs *x509.CertPool - if p.c.CABundle != nil { - rootCAs = x509.NewCertPool() - if !rootCAs.AppendCertsFromPEM(p.c.CABundle) { - return nil, fmt.Errorf("could not parse CA bundle") - } - } - return &tls.Config{MinVersion: tls.VersionTLS12, RootCAs: rootCAs}, nil -} - -// A name for this upstream provider. -func (p *Provider) GetName() string { - return p.c.Name -} - -// Return a URL which uniquely identifies this LDAP provider, e.g. "ldaps://host.example.com:1234?base=user-search-base". -// 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() *url.URL { - u := &url.URL{Scheme: ldapsScheme, Host: p.c.Host} - q := u.Query() - q.Set("base", p.c.UserSearch.Base) - u.RawQuery = q.Encode() - return u -} - -// 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) 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 -} - -// 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) DryRunAuthenticateUser(ctx context.Context, username string) (*authenticator.Response, bool, error) { - endUserBindFunc := func(conn upstreamldap.Conn, foundUserDN string) error { - // Act as if the end user bind always succeeds. - return nil - } - return p.authenticateUserImpl(ctx, username, endUserBindFunc) -} - -// 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 upstreamldap.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 upstreamldap.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, mappedGroupNames, 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 - } - - response := &authenticator.Response{ - User: &user.DefaultInfo{ - Name: mappedUsername, - UID: mappedUID, - Groups: mappedGroupNames, - }, - } - p.traceAuthSuccess(t) - return response, true, nil -} - -func (p *Provider) searchGroupsForUserDN(conn upstreamldap.Conn, userDN string) ([]string, error) { - searchResult, err := conn.SearchWithPaging(p.groupSearchRequest(userDN), groupSearchPageSize) - if err != nil { - return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err) - } - - groupAttributeName := p.c.GroupSearch.GroupNameAttribute - if len(groupAttributeName) == 0 { - groupAttributeName = distinguishedNameAttributeName - } - - groups := []string{} - for _, groupEntry := range searchResult.Entries { - if len(groupEntry.DN) == 0 { - return nil, fmt.Errorf(`searching for group memberships for user with DN %q resulted in search result without DN`, userDN) - } - mappedGroupName, err := p.getSearchResultAttributeValue(groupAttributeName, groupEntry, userDN) - if err != nil { - return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err) - } - groups = append(groups, mappedGroupName) - } - - return groups, nil -} - -func (p *Provider) validateConfig() error { - // TODO if user search base is nil then host must be an IP address? - if p.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 upstreamldap.Conn, username string, bindFunc func(conn upstreamldap.Conn, foundUserDN string) error) (string, string, []string, error) { - searchResult, err := conn.Search(p.userSearchRequest(username)) - if err != nil { - plog.All(`error searching for user`, - "upstreamName", p.GetName(), - "username", username, - "err", err, - ) - return "", "", nil, fmt.Errorf(`error searching for user: %w`, err) - } - if len(searchResult.Entries) == 0 { - if plog.Enabled(plog.LevelAll) { - plog.All("error finding user: user not found (if this username is valid, please check the user search configuration)", - "upstreamName", p.GetName(), - "username", username, - ) - } else { - plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName()) - } - return "", "", nil, nil - } - - // At this point, we have matched at least one entry, so we can be confident that the username is not actually - // someone's password mistakenly entered into the username field, so we can log it without concern. - if len(searchResult.Entries) > 1 { - return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`, - username, len(searchResult.Entries), - ) - } - userEntry := searchResult.Entries[0] - if len(userEntry.DN) == 0 { - return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username) - } - - mappedUsername, err := p.getSearchResultAttributeValue(p.usernameAttribute(), userEntry, username) - if err != nil { - return "", "", nil, err - } - - mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.uidAttribute(), userEntry, username) - if err != nil { - return "", "", nil, err - } - - mappedGroupNames := []string{} - if len(p.c.GroupSearch.Base) > 0 { - mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN) - if err != nil { - return "", "", nil, err - } - } - sort.Strings(mappedGroupNames) - - // Caution: Note that any other LDAP commands after this bind will be run as this user instead of as the configured BindUsername! - err = bindFunc(conn, userEntry.DN) - 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) - ldapErr := &ldap.Error{} - if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials { - return "", "", nil, nil - } - return "", "", nil, fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) - } - - return mappedUsername, mappedUID, mappedGroupNames, nil -} - -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.userSearchBase(), - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - 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) userSearchBase() string { - if len(p.c.UserSearch.Base) == 0 { - return "" - } - return p.c.UserSearch.Base -} - -func (p *Provider) groupSearchRequest(userDN string) *ldap.SearchRequest { - // See https://ldap.com/the-ldap-search-operation for general documentation of LDAP search options. - return &ldap.SearchRequest{ - BaseDN: p.c.GroupSearch.Base, - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - SizeLimit: 0, // unlimited size because we will search with paging - TimeLimit: 90, - TypesOnly: false, - Filter: p.groupSearchFilter(userDN), - Attributes: p.groupSearchRequestedAttributes(), - Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us - } -} - -func (p *Provider) userSearchRequestedAttributes() []string { - attributes := []string{} - if p.usernameAttribute() != distinguishedNameAttributeName { - attributes = append(attributes, p.usernameAttribute()) - } - if p.uidAttribute() != distinguishedNameAttributeName { - attributes = append(attributes, p.uidAttribute()) - } - return attributes -} - -func (p *Provider) groupSearchRequestedAttributes() []string { - switch p.c.GroupSearch.GroupNameAttribute { - case "": - return []string{} - case distinguishedNameAttributeName: - return []string{} - default: - return []string{p.c.GroupSearch.GroupNameAttribute} - } -} - -func (p *Provider) usernameAttribute() string { - if len(p.c.UserSearch.UsernameAttribute) == 0 { - return sAMAccountNameAttributeName - } - return p.c.UserSearch.UsernameAttribute -} - -func (p *Provider) uidAttribute() string { - if len(p.c.UserSearch.UIDAttribute) == 0 { - return objectGUIDAttributeName - } - return p.c.UserSearch.UIDAttribute -} - -func (p *Provider) userSearchFilter(username string) string { - safeUsername := p.escapeUsernameForSearchFilter(username) - if len(p.c.UserSearch.Filter) == 0 { - return fmt.Sprintf("(%s=%s)", p.usernameAttribute(), safeUsername) - } - return interpolateSearchFilter(p.c.UserSearch.Filter, safeUsername) -} - -func (p *Provider) groupSearchFilter(userDN string) string { - if len(p.c.GroupSearch.Filter) == 0 { - return fmt.Sprintf("(member=%s)", userDN) - } - return interpolateSearchFilter(p.c.GroupSearch.Filter, userDN) -} - -func interpolateSearchFilter(filterFormat, valueToInterpolateIntoFilter string) string { - filter := strings.ReplaceAll(filterFormat, searchFilterInterpolationLocationMarker, valueToInterpolateIntoFilter) - if strings.HasPrefix(filter, "(") && strings.HasSuffix(filter, ")") { - return filter - } - return "(" + filter + ")" -} - -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) -} - -// Returns the (potentially) binary data of the attribute's value, base64 URL encoded. -func (p *Provider) getSearchResultAttributeRawValueEncoded(attributeName string, entry *ldap.Entry, username string) (string, error) { - if attributeName == distinguishedNameAttributeName { - return base64.RawURLEncoding.EncodeToString([]byte(entry.DN)), nil - } - - attributeValues := entry.GetRawAttributeValues(attributeName) - - if len(attributeValues) != 1 { - 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 { - return "", fmt.Errorf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, - attributeName, username, - ) - } - - if attributeName == objectGUIDAttributeName { - uuidEntry, err := uuid.FromBytes(attributeValue) - if err != nil { - return "", fmt.Errorf("Error decoding UID: %s", err.Error()) - } - return uuidEntry.String(), nil - } - - return base64.RawURLEncoding.EncodeToString(attributeValue), nil -} - -func (p *Provider) getSearchResultAttributeValue(attributeName string, entry *ldap.Entry, username string) (string, error) { - if attributeName == distinguishedNameAttributeName { - return entry.DN, nil - } - - attributeValues := entry.GetAttributeValues(attributeName) - - if len(attributeValues) != 1 { - 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 { - 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 -} - -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}, - ) -} diff --git a/internal/upstreamad/upstreamad_test.go b/internal/upstreamad/upstreamad_test.go deleted file mode 100644 index 579bddd27..000000000 --- a/internal/upstreamad/upstreamad_test.go +++ /dev/null @@ -1,1377 +0,0 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package upstreamad - -import ( - "context" - "crypto/tls" - "encoding/base64" - "errors" - "fmt" - "net" - "net/http" - "net/url" - "testing" - "time" - - "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/certauthority" - "go.pinniped.dev/internal/endpointaddr" - "go.pinniped.dev/internal/mocks/mockldapconn" - "go.pinniped.dev/internal/testutil" - "go.pinniped.dev/internal/upstreamldap" -) - -const ( - testHost = "activedirectory.example.com:8443" - testBindUsername = "cn=some-bind-username,dc=pinniped,dc=dev" - testBindPassword = "some-bind-password" - testUpstreamUsername = "some-upstream-username" - testUpstreamPassword = "some-upstream-password" - testUserSearchBase = "some-upstream-user-base-dn" - testGroupSearchBase = "some-upstream-group-base-dn" - testUserSearchFilter = "some-user-filter={}-and-more-filter={}" - testGroupSearchFilter = "some-group-filter={}-and-more-filter={}" - testUserSearchUsernameAttribute = "some-upstream-username-attribute" - testUserSearchUIDAttribute = "objectGUID" - testGroupSearchGroupNameAttribute = "some-upstream-group-name-attribute" - testUserSearchResultDNValue = "some-upstream-user-dn" - testGroupSearchResultDNValue1 = "some-upstream-group-dn1" - testGroupSearchResultDNValue2 = "some-upstream-group-dn2" - testUserSearchResultUsernameAttributeValue = "some-upstream-username-value" - testUserSearchResultUIDAttributeValue = "\x12>Eg\xe8\x9b\x12\u04e4VBf\x14\x17@\x00" // binary representation of 123e4567-e89b-12d3-a456-426614174000 - testGroupSearchResultGroupNameAttributeValue1 = "some-upstream-group-name-value1" - testGroupSearchResultGroupNameAttributeValue2 = "some-upstream-group-name-value2" - - expectedGroupSearchPageSize = uint32(250) -) - -var ( - testUserSearchFilterInterpolated = fmt.Sprintf("(some-user-filter=%s-and-more-filter=%s)", testUpstreamUsername, testUpstreamUsername) - testGroupSearchFilterInterpolated = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", testUserSearchResultDNValue, testUserSearchResultDNValue) -) - -func TestEndUserAuthentication(t *testing.T) { - providerConfig := func(editFunc func(p *upstreamldap.ProviderConfig)) *upstreamldap.ProviderConfig { - config := &upstreamldap.ProviderConfig{ - Name: "some-provider-name", - Host: testHost, - CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: upstreamldap.TLS, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: upstreamldap.UserSearchConfig{ - Base: testUserSearchBase, - Filter: testUserSearchFilter, - UsernameAttribute: testUserSearchUsernameAttribute, - UIDAttribute: testUserSearchUIDAttribute, - }, - GroupSearch: upstreamldap.GroupSearchConfig{ - Base: testGroupSearchBase, - Filter: testGroupSearchFilter, - GroupNameAttribute: testGroupSearchGroupNameAttribute, - }, - } - if editFunc != nil { - editFunc(config) - } - return config - } - - expectedUserSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { - request := &ldap.SearchRequest{ - BaseDN: testUserSearchBase, - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - SizeLimit: 2, - TimeLimit: 90, - TypesOnly: false, - Filter: testUserSearchFilterInterpolated, - Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute}, - Controls: nil, // don't need paging because we set the SizeLimit so small - } - if editFunc != nil { - editFunc(request) - } - return request - } - - expectedGroupSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { - request := &ldap.SearchRequest{ - BaseDN: testGroupSearchBase, - Scope: ldap.ScopeWholeSubtree, - DerefAliases: ldap.NeverDerefAliases, - SizeLimit: 0, // unlimited size because we will search with paging - TimeLimit: 90, - TypesOnly: false, - Filter: testGroupSearchFilterInterpolated, - Attributes: []string{testGroupSearchGroupNameAttribute}, - Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us - } - if editFunc != nil { - editFunc(request) - } - return request - } - - exampleUserSearchResult := &ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), - }, - }, - }, - } - - exampleGroupSearchResult := &ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), - }, - }, - { - DN: testGroupSearchResultDNValue2, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue2}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - } - - // The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult. - expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticator.Response { - u := &user.DefaultInfo{ - Name: testUserSearchResultUsernameAttributeValue, - UID: "123e4567-e89b-12d3-a456-426614174000", - Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, - } - if editFunc != nil { - editFunc(u) - } - return &authenticator.Response{User: u} - } - - tests := []struct { - name string - username string - password string - providerConfig *upstreamldap.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), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "default as much as possible", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: &upstreamldap.ProviderConfig{ - Name: "some-provider-name", - Host: testHost, - CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: upstreamldap.TLS, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - // no user search... that's all defaulted. - GroupSearch: upstreamldap.GroupSearchConfig{ - Base: testGroupSearchBase, - Filter: testGroupSearchFilter, - GroupNameAttribute: testGroupSearchGroupNameAttribute, - }, - }, - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { - r.Filter = "(" + sAMAccountNameAttributeName + "=" + testUpstreamUsername + ")" - r.Attributes = []string{sAMAccountNameAttributeName, testUserSearchUIDAttribute} - r.BaseDN = "" - })).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(sAMAccountNameAttributeName, []string{testUserSearchResultUsernameAttributeValue}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), - }, - }, - }, - }, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "when the user search filter is already wrapped by parenthesis then it is not wrapped again", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.UserSearch.Filter = "(" + testUserSearchFilter + ")" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "when the group search filter is already wrapped by parenthesis then it is not wrapped again", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.GroupSearch.Filter = "(" + testGroupSearchFilter + ")" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "when the group search base is empty then skip the group search entirely", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.GroupSearch.Base = "" // this configuration means that the user does not want group search to happen - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { - r.Groups = []string{} - }), - }, - { - name: "when the UsernameAttribute is dn and there is a user search filter provided", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.UserSearch.UsernameAttribute = "dn" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { - r.Attributes = []string{testUserSearchUIDAttribute} - })).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), - }, - }, - }, - }, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { - r.Name = testUserSearchResultDNValue - }), - }, - { - name: "when the UIDAttribute is dn", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.UserSearch.UIDAttribute = "dn" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { - r.Attributes = []string{testUserSearchUsernameAttribute} - })).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), - }, - }, - }, - }, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { - r.UID = base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultDNValue)) - }), - }, - { - name: "when the GroupNameAttribute is empty then it defaults to dn", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.GroupSearch.GroupNameAttribute = "" // blank means to use dn - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { - r.Attributes = []string{} - }), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { - r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2} - }), - }, - { - name: "when the GroupNameAttribute is dn", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.GroupSearch.GroupNameAttribute = "dn" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { - r.Attributes = []string{} - }), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { - r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2} - }), - }, - { - name: "when the GroupNameAttribute is cn", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.GroupSearch.GroupNameAttribute = "cn" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { - r.Attributes = []string{"cn"} - }), expectedGroupSearchPageSize). - Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute("cn", []string{testGroupSearchResultGroupNameAttributeValue1}), - }, - }, - { - DN: testGroupSearchResultDNValue2, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute("cn", []string{testGroupSearchResultGroupNameAttributeValue2}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "when user search Filter is blank it derives a search filter from the UsernameAttribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.UserSearch.Filter = "" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { - r.Filter = "(" + testUserSearchUsernameAttribute + "=" + testUpstreamUsername + ")" - })).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "when user search Filter and user attribute is blank it defaults to sAMAccountName={}", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.UserSearch.Filter = "" - p.UserSearch.UsernameAttribute = "" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { - r.Filter = "(" + sAMAccountNameAttributeName + "=" + testUpstreamUsername + ")" - r.Attributes = []string{sAMAccountNameAttributeName, testUserSearchUIDAttribute} - })).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(sAMAccountNameAttributeName, []string{testUserSearchResultUsernameAttributeValue}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), - }, - }, - }, - }, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "when group search Filter is blank it uses a default search filter of member={}", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - p.GroupSearch.Filter = "" - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { - r.Filter = "(member=" + testUserSearchResultDNValue + ")" - }), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "when the username has special LDAP search filter characters then they must be properly escaped in the search filter, because the username is end-user input", - username: `a&b|c(d)e\f*g`, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { - r.Filter = fmt.Sprintf("(some-user-filter=%s-and-more-filter=%s)", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) - })).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(nil), - }, - { - name: "group names are sorted to make the result more stable/predictable", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{"c"}), - }, - }, - { - DN: testGroupSearchResultDNValue2, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{"a"}), - }, - }, - { - DN: testGroupSearchResultDNValue2, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{"b"}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{ - Name: testUserSearchResultUsernameAttributeValue, - UID: "123e4567-e89b-12d3-a456-426614174000", - Groups: []string{"a", "b", "c"}, - }, - }, - }, - { - 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, - providerConfig: providerConfig(func(p *upstreamldap.ProviderConfig) { - 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", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: 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, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(nil, errors.New("some user search error")).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantError: `error searching for user: some user search error`, - }, - { - name: "when searching for the user's groups returns an error", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(nil, errors.New("some group search error")).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantError: fmt.Sprintf(`error searching for group memberships for user with DN "%s": some group search error`, testUserSearchResultDNValue), - }, - { - name: "when searching for the user returns no results", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantUnauthenticated: true, - }, - { - name: "when searching for the user returns multiple results", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - {DN: testUserSearchResultDNValue}, - {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, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(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's groups returns a group without a DN", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), - }, - }, - { - DN: "", - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue2}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantError: fmt.Sprintf( - `searching for group memberships for user with DN "%s" resulted in search result without DN`, - testUserSearchResultDNValue), - }, - { - name: "when searching for the user returns a user without an expected username attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), - }, - }, - }, - }, 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 group memberships returns a group without an expected group name attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), - }, - }, - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute("unrelated attribute", []string{"anything"}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantError: fmt.Sprintf( - `error searching for group memberships for user with DN "%s": found 0 values for attribute "%s" while searching for user "%s", but expected 1 result`, - testUserSearchResultDNValue, testGroupSearchGroupNameAttribute, testUserSearchResultDNValue), - }, - { - 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), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{ - testUserSearchResultUsernameAttributeValue, - "unexpected-additional-value", - }), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), - }, - }, - }, - }, 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 group memberships returns a group with too many values for the expected group name attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), - }, - }, - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{ - testGroupSearchResultGroupNameAttributeValue1, - "unexpected-additional-value", - }), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantError: fmt.Sprintf( - `error searching for group memberships for user with DN "%s": found 2 values for attribute "%s" while searching for user "%s", but expected 1 result`, - testUserSearchResultDNValue, testGroupSearchGroupNameAttribute, testUserSearchResultDNValue), - }, - { - 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), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{""}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), - }, - }, - }, - }, 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 group memberships returns a group with an empty value for for the expected group name attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}), - }, - }, - { - DN: testGroupSearchResultDNValue1, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{""}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantError: fmt.Sprintf( - `error searching for group memberships for user with DN "%s": found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`, - testUserSearchResultDNValue, testGroupSearchGroupNameAttribute, testUserSearchResultDNValue), - }, - { - name: "when searching for the user returns a user without an expected UID attribute", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), - }, - }, - }, - }, 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, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{ - testUserSearchResultUIDAttributeValue, - "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, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testUserSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), - 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, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, 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, testUserSearchResultDNValue), - }, - { - name: "when binding as the found user returns a specific invalid credentials error", - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). - Return(exampleGroupSearchResult, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - wantUnauthenticated: true, - skipDryRunAuthenticateUser: true, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - err := &ldap.Error{ - Err: errors.New("some bind error"), - ResultCode: ldap.LDAPResultInvalidCredentials, - } - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Return(err).Times(1) - }, - }, - { - name: "when no username is specified", - username: "", - password: testUpstreamPassword, - providerConfig: providerConfig(nil), - wantToSkipDial: true, - wantUnauthenticated: true, - }, - } - - 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.searchMocks != nil { - tt.searchMocks(conn) - } - if tt.bindEndUserMocks != nil { - tt.bindEndUserMocks(conn) - } - - dialWasAttempted := false - tt.providerConfig.Dialer = upstreamldap.LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { - dialWasAttempted = true - require.Equal(t, tt.providerConfig.Host, addr.Endpoint()) - if tt.dialError != nil { - return nil, tt.dialError - } - return conn, nil - }) - - 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) - 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) - } - }) - } -} - -func TestTestConnection(t *testing.T) { - providerConfig := func(editFunc func(p *upstreamldap.ProviderConfig)) *upstreamldap.ProviderConfig { - config := &upstreamldap.ProviderConfig{ - Name: "some-provider-name", - Host: testHost, - CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test - ConnectionProtocol: upstreamldap.TLS, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: upstreamldap.UserSearchConfig{}, // not used by TestConnection - } - if editFunc != nil { - editFunc(config) - } - return config - } - - tests := []struct { - name string - providerConfig *upstreamldap.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 *upstreamldap.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 = upstreamldap.LDAPDialerFunc(func(ctx context.Context, addr endpointaddr.HostPort) (upstreamldap.Conn, error) { - dialWasAttempted = true - require.Equal(t, tt.providerConfig.Host, addr.Endpoint()) - 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 := upstreamldap.ProviderConfig{ - Name: "original-provider-name", - Host: testHost, - CABundle: []byte("some-ca-bundle"), - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: upstreamldap.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?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev", - New(upstreamldap.ProviderConfig{ - Host: "ldap.example.com:1234", - UserSearch: upstreamldap.UserSearchConfig{Base: "ou=users,dc=pinniped,dc=dev"}, - }).GetURL().String()) - - require.Equal(t, - "ldaps://ldap.example.com?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev", - New(upstreamldap.ProviderConfig{ - Host: "ldap.example.com", - UserSearch: upstreamldap.UserSearchConfig{Base: "ou=users,dc=pinniped,dc=dev"}, - }).GetURL().String()) -} - -// 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 - - caForTestServerWithBadCertName, err := certauthority.New("Test CA", time.Hour) - require.NoError(t, err) - wrongIP := net.ParseIP("10.2.3.4") - cert, err := caForTestServerWithBadCertName.IssueServerCert([]string{"wrong-dns-name"}, []net.IP{wrongIP}, time.Hour) - require.NoError(t, err) - testServerWithBadCertNameAddr := testutil.TLSTestServerWithCert(t, func(w http.ResponseWriter, r *http.Request) {}, cert) - - 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 - connProto upstreamldap.LDAPConnectionProtocol - caBundle []byte - context context.Context - wantError string - }{ - { - name: "happy path", - host: testServerHostAndPort, - caBundle: []byte(testServerCABundle), - connProto: upstreamldap.TLS, - context: context.Background(), - }, - { - name: "server cert name does not match the address to which the client connected", - host: testServerWithBadCertNameAddr, - caBundle: caForTestServerWithBadCertName.Bundle(), - connProto: upstreamldap.TLS, - context: context.Background(), - wantError: `LDAP Result Code 200 "Network Error": x509: certificate is valid for 10.2.3.4, not 127.0.0.1`, - }, - { - name: "invalid CA bundle with TLS", - host: testServerHostAndPort, - caBundle: []byte("not a ca bundle"), - connProto: upstreamldap.TLS, - context: context.Background(), - wantError: `LDAP Result Code 200 "Network Error": could not parse CA bundle`, - }, - { - name: "invalid CA bundle with StartTLS", - host: testServerHostAndPort, - caBundle: []byte("not a ca bundle"), - connProto: upstreamldap.StartTLS, - context: context.Background(), - wantError: `LDAP Result Code 200 "Network Error": could not parse CA bundle`, - }, - { - name: "invalid host with TLS", - host: "this:is:not:a:valid:hostname", - caBundle: []byte(testServerCABundle), - connProto: upstreamldap.TLS, - context: context.Background(), - wantError: `LDAP Result Code 200 "Network Error": host "this:is:not:a:valid:hostname" is not a valid hostname or IP address`, - }, - { - name: "invalid host with StartTLS", - host: "this:is:not:a:valid:hostname", - caBundle: []byte(testServerCABundle), - connProto: upstreamldap.StartTLS, - context: context.Background(), - wantError: `LDAP Result Code 200 "Network Error": host "this:is:not:a:valid:hostname" is not a valid hostname or IP address`, - }, - { - name: "missing CA bundle when it is required because the host is not using a trusted CA", - host: testServerHostAndPort, - caBundle: nil, - connProto: upstreamldap.TLS, - 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), - connProto: upstreamldap.TLS, - 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), - connProto: upstreamldap.TLS, - context: alreadyCancelledContext, - wantError: fmt.Sprintf(`LDAP Result Code 200 "Network Error": dial tcp %s: operation was canceled`, testServerHostAndPort), - }, - { - name: "unsupported connection protocol", - host: testServerHostAndPort, - caBundle: []byte(testServerCABundle), - connProto: "bad usage of this type", - context: alreadyCancelledContext, - wantError: `LDAP Result Code 200 "Network Error": did not specify valid ConnectionProtocol`, - }, - } - for _, test := range tests { - tt := test - t.Run(tt.name, func(t *testing.T) { - provider := New(upstreamldap.ProviderConfig{ - Host: tt.host, - CABundle: tt.caBundle, - ConnectionProtocol: tt.connProto, - Dialer: nil, // this test is for the default (production) TLS dialer - }) - conn, err := provider.dial(tt.context) - if conn != nil { - defer conn.Close() - } - if tt.wantError != "" { - require.Nil(t, conn) - require.EqualError(t, err, tt.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 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 e5c8cbb3a456e1a0cb0f0dce6638f93a0d7f3048 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 15 Jul 2021 16:46:16 -0700 Subject: [PATCH 11/36] One line fix for lint error. Forgot a period in a comment. Signed-off-by: Ryan Richard --- .../active_directory_upstream_watcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 0394bdada..be69e76ec 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -43,7 +43,7 @@ const ( noTLSConfigurationMessage = "no TLS configuration provided" loadedTLSConfigurationMessage = "loaded TLS configuration" - // Default values for active directory config + // Default values for active directory config. defaultActiveDirectoryUsernameAttributeName = "sAMAccountName" defaultActiveDirectoryUIDAttributeName = "objectGUID" ) From 3b4f521596b68d4ff5135459390ca85f183620db Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Thu, 15 Jul 2021 16:58:26 -0700 Subject: [PATCH 12/36] Changed TestLDAPUpstream.TestUsernameAttributeName back to TestUserMailAttributeName Also added TestUserSAMAccountNameValue Signed-off-by: Margo Crawford --- test/integration/e2e_test.go | 4 ++-- test/integration/supervisor_login_test.go | 12 ++++++------ test/testlib/env.go | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 1eb8f724c..fd631822b 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -386,7 +386,7 @@ func TestE2EFullIntegration(t *testing.T) { t.Skip("LDAP integration test requires connectivity to an LDAP server") } - expectedUsername := env.SupervisorUpstreamLDAP.TestUsernameAttributeValue + expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. @@ -422,7 +422,7 @@ func TestE2EFullIntegration(t *testing.T) { Base: env.SupervisorUpstreamLDAP.UserSearchBase, Filter: "", Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUsernameAttributeName, + Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index bfb938867..8b449db88 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -128,7 +128,7 @@ func TestSupervisorLogin(t *testing.T) { Base: env.SupervisorUpstreamLDAP.UserSearchBase, Filter: "", Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUsernameAttributeName, + Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, @@ -150,7 +150,7 @@ func TestSupervisorLogin(t *testing.T) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorizationUsingLDAPIdentityProvider(t, downstreamAuthorizeURL, - env.SupervisorUpstreamLDAP.TestUsernameAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login httpClient, ) @@ -162,7 +162,7 @@ func TestSupervisorLogin(t *testing.T) { "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUsernameAttributeValue), + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { @@ -274,8 +274,8 @@ func TestSupervisorLogin(t *testing.T) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorizationUsingLDAPIdentityProvider(t, downstreamAuthorizeURL, - env.SupervisorUpstreamActiveDirectory.TestUsernameAttributeValue, // username to present to server during login - env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login httpClient, ) }, @@ -286,7 +286,7 @@ func TestSupervisorLogin(t *testing.T) { "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUsernameAttributeValue), + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs, }, } diff --git a/test/testlib/env.go b/test/testlib/env.go index 64706e589..04c770e01 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -92,12 +92,13 @@ type TestLDAPUpstream struct { TestUserDN string `json:"testUserDN"` TestUserCN string `json:"testUserCN"` TestUserPassword string `json:"testUserPassword"` - TestUsernameAttributeName string `json:"testUserMailAttributeName"` - TestUsernameAttributeValue string `json:"testUserMailAttributeValue"` + TestUserMailAttributeName string `json:"testUserMailAttributeName"` + TestUserMailAttributeValue string `json:"testUserMailAttributeValue"` TestUserUniqueIDAttributeName string `json:"testUserUniqueIDAttributeName"` TestUserUniqueIDAttributeValue string `json:"testUserUniqueIDAttributeValue"` TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` TestUserDirectGroupsDNs []string `json:"testUserDirectGroupsDNs"` //nolint:golint // this is "distinguished names", not "DNS" + TestUserSAMAccountNameValue string `json:"testUserSAMAccountNameValue"` } // ProxyEnv returns a set of environment variable strings (e.g., to combine with os.Environ()) which set up the configured test HTTP proxy. @@ -261,8 +262,8 @@ func loadEnvVars(t *testing.T, result *TestEnv) { 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"), - TestUsernameAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME"), - TestUsernameAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE"), + TestUserMailAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME"), + TestUserMailAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE"), TestUserDirectGroupsCNs: filterEmpty(strings.Split(needEnv(t, "PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_CN"), ";")), TestUserDirectGroupsDNs: filterEmpty(strings.Split(needEnv(t, "PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_DN"), ";")), TestUserPassword: needEnv(t, "PINNIPED_TEST_LDAP_USER_PASSWORD"), @@ -276,8 +277,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), - TestUsernameAttributeName: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_NAME", ""), - TestUsernameAttributeValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), } From 5d8d7246c2744d34e81399f0d9b120adfc034959 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Mon, 19 Jul 2021 13:54:07 -0700 Subject: [PATCH 13/36] Refactor active directory and ldap controllers to share almost everything Signed-off-by: Ryan Richard --- .../active_directory_upstream_watcher.go | 339 ++++++------------ .../active_directory_upstream_watcher_test.go | 69 ++-- .../ldap_upstream_watcher.go | 331 +++++------------ .../ldap_upstream_watcher_test.go | 41 +-- .../upstreamwatchers/upstream_watchers.go | 307 +++++++++++++++- test/integration/supervisor_login_test.go | 4 +- 6 files changed, 562 insertions(+), 529 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index be69e76ec..27357bfeb 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -6,12 +6,8 @@ package activedirectoryupstreamwatcher import ( "context" - "crypto/x509" - "encoding/base64" "fmt" - "time" - 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" @@ -26,28 +22,109 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) const ( - activeDirectoryControllerName = "active-directory-upstream-observer" - activeDirectoryBindAccountSecretType = corev1.SecretTypeBasicAuth - testActiveDirectoryConnectionTimeout = 90 * time.Second - - // Constants related to conditions. - typeBindSecretValid = "BindSecretValid" - typeTLSConfigurationValid = "TLSConfigurationValid" - typeActiveDirectoryConnectionValid = "ActiveDirectoryConnectionValid" - reasonActiveDirectoryConnectionError = "ActiveDirectoryConnectionError" - noTLSConfigurationMessage = "no TLS configuration provided" - loadedTLSConfigurationMessage = "loaded TLS configuration" + activeDirectoryControllerName = "active-directory-upstream-observer" // Default values for active directory config. defaultActiveDirectoryUsernameAttributeName = "sAMAccountName" defaultActiveDirectoryUIDAttributeName = "objectGUID" ) +type activeDirectoryUpstreamGenericLDAPImpl struct { + activeDirectoryIdentityProvider v1alpha1.ActiveDirectoryIdentityProvider +} + +func (g *activeDirectoryUpstreamGenericLDAPImpl) Spec() upstreamwatchers.UpstreamGenericLDAPSpec { + return &activeDirectoryUpstreamGenericLDAPSpec{g.activeDirectoryIdentityProvider} +} + +func (g *activeDirectoryUpstreamGenericLDAPImpl) Namespace() string { + return g.activeDirectoryIdentityProvider.Namespace +} + +func (g *activeDirectoryUpstreamGenericLDAPImpl) Name() string { + return g.activeDirectoryIdentityProvider.Name +} + +func (g *activeDirectoryUpstreamGenericLDAPImpl) Generation() int64 { + return g.activeDirectoryIdentityProvider.Generation +} + +func (g *activeDirectoryUpstreamGenericLDAPImpl) Status() upstreamwatchers.UpstreamGenericLDAPStatus { + return &activeDirectoryUpstreamGenericLDAPStatus{g.activeDirectoryIdentityProvider} +} + +type activeDirectoryUpstreamGenericLDAPSpec struct { + activeDirectoryIdentityProvider v1alpha1.ActiveDirectoryIdentityProvider +} + +func (s *activeDirectoryUpstreamGenericLDAPSpec) Host() string { + return s.activeDirectoryIdentityProvider.Spec.Host +} + +func (s *activeDirectoryUpstreamGenericLDAPSpec) TLSSpec() *v1alpha1.TLSSpec { + return s.activeDirectoryIdentityProvider.Spec.TLS +} + +func (s *activeDirectoryUpstreamGenericLDAPSpec) BindSecretName() string { + return s.activeDirectoryIdentityProvider.Spec.Bind.SecretName +} + +func (s *activeDirectoryUpstreamGenericLDAPSpec) UserSearch() upstreamwatchers.UpstreamGenericLDAPUserSearch { + return &activeDirectoryUpstreamGenericLDAPUserSearch{s.activeDirectoryIdentityProvider.Spec.UserSearch} +} + +func (s *activeDirectoryUpstreamGenericLDAPSpec) GroupSearch() upstreamwatchers.UpstreamGenericLDAPGroupSearch { + return &activeDirectoryUpstreamGenericLDAPGroupSearch{s.activeDirectoryIdentityProvider.Spec.GroupSearch} +} + +type activeDirectoryUpstreamGenericLDAPUserSearch struct { + userSearch v1alpha1.ActiveDirectoryIdentityProviderUserSearch +} + +func (u *activeDirectoryUpstreamGenericLDAPUserSearch) Base() string { + return u.userSearch.Base +} + +func (u *activeDirectoryUpstreamGenericLDAPUserSearch) Filter() string { + return u.userSearch.Filter +} + +func (u *activeDirectoryUpstreamGenericLDAPUserSearch) UsernameAttribute() string { + return u.userSearch.Attributes.Username +} + +func (u *activeDirectoryUpstreamGenericLDAPUserSearch) UIDAttribute() string { + return u.userSearch.Attributes.UID +} + +type activeDirectoryUpstreamGenericLDAPGroupSearch struct { + groupSearch v1alpha1.ActiveDirectoryIdentityProviderGroupSearch +} + +func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) Base() string { + return g.groupSearch.Base +} + +func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) Filter() string { + return g.groupSearch.Filter +} + +func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) GroupNameAttribute() string { + return g.groupSearch.Attributes.GroupName +} + +type activeDirectoryUpstreamGenericLDAPStatus struct { + activeDirectoryIdentityProvider v1alpha1.ActiveDirectoryIdentityProvider +} + +func (s *activeDirectoryUpstreamGenericLDAPStatus) Conditions() []v1alpha1.Condition { + return s.activeDirectoryIdentityProvider.Status.Conditions +} + // UpstreamActiveDirectoryIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. type UpstreamActiveDirectoryIdentityProviderICache interface { SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) @@ -55,28 +132,13 @@ type UpstreamActiveDirectoryIdentityProviderICache interface { type activeDirectoryWatcherController struct { cache UpstreamActiveDirectoryIdentityProviderICache - validatedSecretVersionsCache *secretVersionCache + validatedSecretVersionsCache *upstreamwatchers.SecretVersionCache ldapDialer upstreamldap.LDAPDialer client pinnipedclientset.Interface activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer secretInformer corev1informers.SecretInformer } -// An in-memory cache with an entry for each ActiveDirectoryIdentityProvider, to keep track of which ResourceVersion -// of the bind Secret and which TLS/StartTLS setting was used during the most recent successful validation. -type secretVersionCache struct { - ValidatedSettingsByName map[string]validatedSettings -} - -type validatedSettings struct { - BindSecretResourceVersion string - LDAPConnectionProtocol upstreamldap.LDAPConnectionProtocol -} - -func newSecretVersionCache() *secretVersionCache { - return &secretVersionCache{ValidatedSettingsByName: map[string]validatedSettings{}} -} - // New instantiates a new controllerlib.Controller which will populate the provided UpstreamActiveDirectoryIdentityProviderICache. func New( idpCache UpstreamActiveDirectoryIdentityProviderICache, @@ -88,7 +150,7 @@ func New( return newInternal( idpCache, // start with an empty secretVersionCache - newSecretVersionCache(), + upstreamwatchers.NewSecretVersionCache(), // nil means to use a real production dialer when creating objects to add to the cache nil, client, @@ -101,7 +163,7 @@ func New( // For test dependency injection purposes. func newInternal( idpCache UpstreamActiveDirectoryIdentityProviderICache, - validatedSecretVersionsCache *secretVersionCache, + validatedSecretVersionsCache *upstreamwatchers.SecretVersionCache, ldapDialer upstreamldap.LDAPDialer, client pinnipedclientset.Interface, activeDirectoryIdentityProviderInformer idpinformers.ActiveDirectoryIdentityProviderInformer, @@ -125,7 +187,7 @@ func newInternal( ), withInformer( secretInformer, - pinnipedcontroller.MatchAnySecretOfTypeFilter(activeDirectoryBindAccountSecretType, pinnipedcontroller.SingletonQueue()), + pinnipedcontroller.MatchAnySecretOfTypeFilter(upstreamwatchers.LDAPBindAccountSecretType, pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), ) @@ -187,212 +249,11 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, Dialer: c.ldapDialer, } - conditions := []*v1alpha1.Condition{} - secretValidCondition, currentSecretVersion := c.validateSecret(upstream, config) - tlsValidCondition := c.validateTLSConfig(upstream, config) - conditions = append(conditions, secretValidCondition, tlsValidCondition) + conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &activeDirectoryUpstreamGenericLDAPImpl{*upstream}, c.secretInformer, c.validatedSecretVersionsCache, config) - // 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) - if finishedConfigCondition != nil { - conditions = append(conditions, finishedConfigCondition) - } - } + c.updateStatus(ctx, upstream, conditions.Conditions()) - 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 p, requeue -} - -func (c *activeDirectoryWatcherController) validateTLSConfig(upstream *v1alpha1.ActiveDirectoryIdentityProvider, config *upstreamldap.ProviderConfig) *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", upstreamwatchers.ErrNoCertificates)) - } - - config.CABundle = bundle - return c.validTLSCondition(loadedTLSConfigurationMessage) -} - -func (c *activeDirectoryWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { - if c.hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion, config) { - return nil - } - - testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testActiveDirectoryConnectionTimeout) - defer cancelFunc() - - condition := c.testConnection(testConnectionTimeout, upstream, config, 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.ValidatedSettingsByName[upstream.GetName()] = validatedSettings{ - BindSecretResourceVersion: currentSecretVersion, - LDAPConnectionProtocol: config.ConnectionProtocol, - } - } - - return condition -} - -func (c *activeDirectoryWatcherController) testConnection( - ctx context.Context, - upstream *v1alpha1.ActiveDirectoryIdentityProvider, - config *upstreamldap.ProviderConfig, - currentSecretVersion string, -) *v1alpha1.Condition { - // First try using TLS. - config.ConnectionProtocol = upstreamldap.TLS - tlsLDAPProvider := upstreamldap.New(*config) - err := tlsLDAPProvider.TestConnection(ctx) - if err != nil { - plog.InfoErr("testing LDAP connection using TLS failed, so trying again with StartTLS", err, "host", config.Host) - // If there was any error, try again with StartTLS instead. - config.ConnectionProtocol = upstreamldap.StartTLS - startTLSLDAPProvider := upstreamldap.New(*config) - startTLSErr := startTLSLDAPProvider.TestConnection(ctx) - if startTLSErr == nil { - plog.Info("testing LDAP connection using StartTLS succeeded", "host", config.Host) - // Successfully able to fall back to using StartTLS, so clear the original - // error and consider the connection test to be successful. - err = nil - } else { - plog.InfoErr("testing LDAP connection using StartTLS also failed", err, "host", config.Host) - // Falling back to StartTLS also failed, so put TLS back into the config - // and consider the connection test to be failed. - config.ConnectionProtocol = upstreamldap.TLS - } - } - - if err != nil { - return &v1alpha1.Condition{ - Type: typeActiveDirectoryConnectionValid, - Status: v1alpha1.ConditionFalse, - Reason: reasonActiveDirectoryConnectionError, - 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: typeActiveDirectoryConnectionValid, - Status: v1alpha1.ConditionTrue, - 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), - } -} - -func (c *activeDirectoryWatcherController) hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.ActiveDirectoryIdentityProvider, currentSecretVersion string, config *upstreamldap.ProviderConfig) bool { - currentGeneration := upstream.Generation - for _, cond := range upstream.Status.Conditions { - if cond.Type == typeActiveDirectoryConnectionValid && 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, if any. - validatedSecretVersion := c.validatedSecretVersionsCache.ValidatedSettingsByName[upstream.GetName()] - if validatedSecretVersion.BindSecretResourceVersion == currentSecretVersion { - // Reload the TLS vs StartTLS setting that was previously validated. - config.ConnectionProtocol = validatedSecretVersion.LDAPConnectionProtocol - return true - } - } - } - return false -} - -func (c *activeDirectoryWatcherController) validTLSCondition(message string) *v1alpha1.Condition { - return &v1alpha1.Condition{ - Type: typeTLSConfigurationValid, - Status: v1alpha1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, - Message: message, - } -} - -func (c *activeDirectoryWatcherController) invalidTLSCondition(message string) *v1alpha1.Condition { - return &v1alpha1.Condition{ - Type: typeTLSConfigurationValid, - Status: v1alpha1.ConditionFalse, - Reason: upstreamwatchers.ReasonInvalidTLSConfig, - Message: message, - } -} - -func (c *activeDirectoryWatcherController) validateSecret(upstream *v1alpha1.ActiveDirectoryIdentityProvider, config *upstreamldap.ProviderConfig) (*v1alpha1.Condition, string) { - secretName := upstream.Spec.Bind.SecretName - - secret, err := c.secretInformer.Lister().Secrets(upstream.Namespace).Get(secretName) - if err != nil { - return &v1alpha1.Condition{ - Type: typeBindSecretValid, - Status: v1alpha1.ConditionFalse, - Reason: upstreamwatchers.ReasonNotFound, - Message: err.Error(), - }, "" - } - - if secret.Type != corev1.SecretTypeBasicAuth { - return &v1alpha1.Condition{ - Type: typeBindSecretValid, - Status: v1alpha1.ConditionFalse, - Reason: upstreamwatchers.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: upstreamwatchers.ReasonMissingKeys, - Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", - secretName, []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey}), - }, secret.ResourceVersion - } - - return &v1alpha1.Condition{ - Type: typeBindSecretValid, - Status: v1alpha1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, - Message: "loaded bind secret", - }, secret.ResourceVersion + return upstreamwatchers.EvaluateConditions(conditions, config) } func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider, conditions []*v1alpha1.Condition) { diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 95468c2a3..f944c710c 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -25,6 +25,7 @@ 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/certauthority" + "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/mocks/mockldapconn" @@ -235,7 +236,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } activeDirectoryConnectionValidTrueCondition := func(gen int64, secretVersion string) v1alpha1.Condition { return v1alpha1.Condition{ - Type: "ActiveDirectoryConnectionValid", + Type: "LDAPConnectionValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -257,8 +258,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } allConditionsTrue := func(gen int64, secretVersion string) []v1alpha1.Condition { return []v1alpha1.Condition{ - activeDirectoryConnectionValidTrueCondition(gen, secretVersion), bindSecretValidTrueCondition(gen), + activeDirectoryConnectionValidTrueCondition(gen, secretVersion), tlsConfigurationValidLoadedTrueCondition(gen), } } @@ -273,7 +274,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { tests := []struct { name string - initialValidatedSettings map[string]validatedSettings + initialValidatedSettings map[string]upstreamwatchers.ValidatedSettings inputUpstreams []runtime.Object inputSecrets []runtime.Object setupMocks func(conn *mockldapconn.MockConn) @@ -281,7 +282,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { wantErr string wantResultingCache []*upstreamldap.ProviderConfig wantResultingUpstreams []v1alpha1.ActiveDirectoryIdentityProvider - wantValidatedSettings map[string]validatedSettings + wantValidatedSettings map[string]upstreamwatchers.ValidatedSettings }{ { name: "no ActiveDirectoryIdentityProvider upstreams clears the cache", @@ -304,7 +305,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "missing secret", @@ -474,8 +475,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ - activeDirectoryConnectionValidTrueCondition(1234, "4242"), bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), { Type: "TLSConfigurationValid", Status: "True", @@ -487,7 +488,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when TLS connection fails it tries to use StartTLS instead: without a specified port it automatically switches ports", @@ -530,8 +531,9 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Phase: "Ready", Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), { - Type: "ActiveDirectoryConnectionValid", + Type: "LDAPConnectionValid", Status: "True", LastTransitionTime: now, Reason: "Success", @@ -540,12 +542,11 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { "ldap.example.com", testBindUsername, testSecretName, "4242"), ObservedGeneration: 1234, }, - bindSecretValidTrueCondition(1234), tlsConfigurationValidLoadedTrueCondition(1234), }, }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, }, { name: "when TLS connection fails it tries to use StartTLS instead: with a specified port it does not automatically switch ports", @@ -587,17 +588,17 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), { - Type: "ActiveDirectoryConnectionValid", + Type: "LDAPConnectionValid", Status: "False", LastTransitionTime: now, - Reason: "ActiveDirectoryConnectionError", + Reason: "LDAPConnectionError", Message: fmt.Sprintf( `could not successfully connect to "%s" and bind as user "%s": error dialing host "%s": some dial error`, "ldap.example.com:5678", testBindUsername, "ldap.example.com:5678"), ObservedGeneration: 1234, }, - bindSecretValidTrueCondition(1234), tlsConfigurationValidLoadedTrueCondition(1234), }, }, @@ -642,7 +643,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", @@ -685,7 +686,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, }, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when testing the connection to the LDAP server fails then the upstream is still added to the cache anyway (treated like a warning)", @@ -704,17 +705,17 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ Phase: "Error", Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), { - Type: "ActiveDirectoryConnectionValid", + Type: "LDAPConnectionValid", Status: "False", LastTransitionTime: now, - Reason: "ActiveDirectoryConnectionError", + 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, }, - bindSecretValidTrueCondition(1234), tlsConfigurationValidLoadedTrueCondition(1234), }, }, @@ -729,7 +730,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, 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. }, @@ -741,7 +742,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when the LDAP server connection was already validated using StartTLS for the current resource generation and secret version, then do not validate it again and keep using StartTLS", @@ -752,7 +753,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, 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. }, @@ -764,7 +765,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, }, { name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again", @@ -775,7 +776,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -789,7 +790,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again", @@ -797,17 +798,17 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { upstream.Generation = 1234 upstream.Status.Conditions = []v1alpha1.Condition{ { - Type: "ActiveDirectoryConnectionValid", + Type: "LDAPConnectionValid", Status: "False", // failure! LastTransitionTime: now, - Reason: "ActiveDirectoryConnectionError", + Reason: "LDAPConnectionError", Message: "some-error-message", ObservedGeneration: 1234, // same (current) generation! }, } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "1", LDAPConnectionProtocol: upstreamldap.TLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "1", LDAPConnectionProtocol: upstreamldap.TLS}}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -821,7 +822,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { 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", @@ -831,8 +832,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { activeDirectoryConnectionValidTrueCondition(1234, "4241"), // same spec generation, old secret version } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS}}, // old version was validated + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS}}, // old version was validated setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -846,7 +847,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when the input activedirectoryidentityprovider leaves user attributes blank, provide default values", @@ -887,7 +888,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, } @@ -923,7 +924,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { return conn, nil })} - validatedSecretVersionCache := newSecretVersionCache() + validatedSecretVersionCache := upstreamwatchers.NewSecretVersionCache() if tt.initialValidatedSettings != nil { validatedSecretVersionCache.ValidatedSettingsByName = tt.initialValidatedSettings } @@ -977,7 +978,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { // Check that the controller remembered which version of the secret it most recently validated successfully with. if tt.wantValidatedSettings == nil { - tt.wantValidatedSettings = map[string]validatedSettings{} + tt.wantValidatedSettings = map[string]upstreamwatchers.ValidatedSettings{} } require.Equal(t, tt.wantValidatedSettings, validatedSecretVersionCache.ValidatedSettingsByName) }) diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index 3f1707b98..968b82a6d 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -6,8 +6,6 @@ package ldapupstreamwatcher import ( "context" - "crypto/x509" - "encoding/base64" "fmt" "time" @@ -26,7 +24,6 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" - "go.pinniped.dev/internal/plog" "go.pinniped.dev/internal/upstreamldap" ) @@ -34,16 +31,100 @@ const ( ldapControllerName = "ldap-upstream-observer" ldapBindAccountSecretType = corev1.SecretTypeBasicAuth 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" ) +type ldapUpstreamGenericLDAPImpl struct { + ldapIdentityProvider v1alpha1.LDAPIdentityProvider +} + +func (g *ldapUpstreamGenericLDAPImpl) Spec() upstreamwatchers.UpstreamGenericLDAPSpec { + return &ldapUpstreamGenericLDAPSpec{g.ldapIdentityProvider} +} + +func (g *ldapUpstreamGenericLDAPImpl) Namespace() string { + return g.ldapIdentityProvider.Namespace +} + +func (g *ldapUpstreamGenericLDAPImpl) Name() string { + return g.ldapIdentityProvider.Name +} + +func (g *ldapUpstreamGenericLDAPImpl) Generation() int64 { + return g.ldapIdentityProvider.Generation +} + +func (g *ldapUpstreamGenericLDAPImpl) Status() upstreamwatchers.UpstreamGenericLDAPStatus { + return &ldapUpstreamGenericLDAPStatus{g.ldapIdentityProvider} +} + +type ldapUpstreamGenericLDAPSpec struct { + ldapIdentityProvider v1alpha1.LDAPIdentityProvider +} + +func (s *ldapUpstreamGenericLDAPSpec) Host() string { + return s.ldapIdentityProvider.Spec.Host +} + +func (s *ldapUpstreamGenericLDAPSpec) TLSSpec() *v1alpha1.TLSSpec { + return s.ldapIdentityProvider.Spec.TLS +} + +func (s *ldapUpstreamGenericLDAPSpec) BindSecretName() string { + return s.ldapIdentityProvider.Spec.Bind.SecretName +} + +func (s *ldapUpstreamGenericLDAPSpec) UserSearch() upstreamwatchers.UpstreamGenericLDAPUserSearch { + return &ldapUpstreamGenericLDAPUserSearch{s.ldapIdentityProvider.Spec.UserSearch} +} + +func (s *ldapUpstreamGenericLDAPSpec) GroupSearch() upstreamwatchers.UpstreamGenericLDAPGroupSearch { + return &ldapUpstreamGenericLDAPGroupSearch{s.ldapIdentityProvider.Spec.GroupSearch} +} + +type ldapUpstreamGenericLDAPUserSearch struct { + userSearch v1alpha1.LDAPIdentityProviderUserSearch +} + +func (u *ldapUpstreamGenericLDAPUserSearch) Base() string { + return u.userSearch.Base +} + +func (u *ldapUpstreamGenericLDAPUserSearch) Filter() string { + return u.userSearch.Filter +} + +func (u *ldapUpstreamGenericLDAPUserSearch) UsernameAttribute() string { + return u.userSearch.Attributes.Username +} + +func (u *ldapUpstreamGenericLDAPUserSearch) UIDAttribute() string { + return u.userSearch.Attributes.UID +} + +type ldapUpstreamGenericLDAPGroupSearch struct { + groupSearch v1alpha1.LDAPIdentityProviderGroupSearch +} + +func (g *ldapUpstreamGenericLDAPGroupSearch) Base() string { + return g.groupSearch.Base +} + +func (g *ldapUpstreamGenericLDAPGroupSearch) Filter() string { + return g.groupSearch.Filter +} + +func (g *ldapUpstreamGenericLDAPGroupSearch) GroupNameAttribute() string { + return g.groupSearch.Attributes.GroupName +} + +type ldapUpstreamGenericLDAPStatus struct { + ldapIdentityProvider v1alpha1.LDAPIdentityProvider +} + +func (s *ldapUpstreamGenericLDAPStatus) Conditions() []v1alpha1.Condition { + return s.ldapIdentityProvider.Status.Conditions +} + // UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. type UpstreamLDAPIdentityProviderICache interface { SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) @@ -51,28 +132,13 @@ type UpstreamLDAPIdentityProviderICache interface { type ldapWatcherController struct { cache UpstreamLDAPIdentityProviderICache - validatedSecretVersionsCache *secretVersionCache + validatedSecretVersionsCache *upstreamwatchers.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 and which TLS/StartTLS setting was used during the most recent successful validation. -type secretVersionCache struct { - ValidatedSettingsByName map[string]validatedSettings -} - -type validatedSettings struct { - BindSecretResourceVersion string - LDAPConnectionProtocol upstreamldap.LDAPConnectionProtocol -} - -func newSecretVersionCache() *secretVersionCache { - return &secretVersionCache{ValidatedSettingsByName: map[string]validatedSettings{}} -} - // New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. func New( idpCache UpstreamLDAPIdentityProviderICache, @@ -84,7 +150,7 @@ func New( return newInternal( idpCache, // start with an empty secretVersionCache - newSecretVersionCache(), + upstreamwatchers.NewSecretVersionCache(), // nil means to use a real production dialer when creating objects to add to the cache nil, client, @@ -97,7 +163,7 @@ func New( // For test dependency injection purposes. func newInternal( idpCache UpstreamLDAPIdentityProviderICache, - validatedSecretVersionsCache *secretVersionCache, + validatedSecretVersionsCache *upstreamwatchers.SecretVersionCache, ldapDialer upstreamldap.LDAPDialer, client pinnipedclientset.Interface, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, @@ -174,212 +240,11 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * Dialer: c.ldapDialer, } - conditions := []*v1alpha1.Condition{} - secretValidCondition, currentSecretVersion := c.validateSecret(upstream, config) - tlsValidCondition := c.validateTLSConfig(upstream, config) - conditions = append(conditions, secretValidCondition, tlsValidCondition) + conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &ldapUpstreamGenericLDAPImpl{*upstream}, c.secretInformer, c.validatedSecretVersionsCache, config) - // 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) - if finishedConfigCondition != nil { - conditions = append(conditions, finishedConfigCondition) - } - } + c.updateStatus(ctx, upstream, conditions.Conditions()) - 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 p, requeue -} - -func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *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", upstreamwatchers.ErrNoCertificates)) - } - - config.CABundle = bundle - return c.validTLSCondition(loadedTLSConfigurationMessage) -} - -func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { - if c.hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion, config) { - return nil - } - - testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout) - defer cancelFunc() - - condition := c.testConnection(testConnectionTimeout, upstream, config, 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.ValidatedSettingsByName[upstream.GetName()] = validatedSettings{ - BindSecretResourceVersion: currentSecretVersion, - LDAPConnectionProtocol: config.ConnectionProtocol, - } - } - - return condition -} - -func (c *ldapWatcherController) testConnection( - ctx context.Context, - upstream *v1alpha1.LDAPIdentityProvider, - config *upstreamldap.ProviderConfig, - currentSecretVersion string, -) *v1alpha1.Condition { - // First try using TLS. - config.ConnectionProtocol = upstreamldap.TLS - tlsLDAPProvider := upstreamldap.New(*config) - err := tlsLDAPProvider.TestConnection(ctx) - if err != nil { - plog.InfoErr("testing LDAP connection using TLS failed, so trying again with StartTLS", err, "host", config.Host) - // If there was any error, try again with StartTLS instead. - config.ConnectionProtocol = upstreamldap.StartTLS - startTLSLDAPProvider := upstreamldap.New(*config) - startTLSErr := startTLSLDAPProvider.TestConnection(ctx) - if startTLSErr == nil { - plog.Info("testing LDAP connection using StartTLS succeeded", "host", config.Host) - // Successfully able to fall back to using StartTLS, so clear the original - // error and consider the connection test to be successful. - err = nil - } else { - plog.InfoErr("testing LDAP connection using StartTLS also failed", err, "host", config.Host) - // Falling back to StartTLS also failed, so put TLS back into the config - // and consider the connection test to be failed. - config.ConnectionProtocol = upstreamldap.TLS - } - } - - 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: 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), - } -} - -func (c *ldapWatcherController) hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string, config *upstreamldap.ProviderConfig) bool { - currentGeneration := upstream.Generation - 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, if any. - validatedSecretVersion := c.validatedSecretVersionsCache.ValidatedSettingsByName[upstream.GetName()] - if validatedSecretVersion.BindSecretResourceVersion == currentSecretVersion { - // Reload the TLS vs StartTLS setting that was previously validated. - config.ConnectionProtocol = validatedSecretVersion.LDAPConnectionProtocol - return true - } - } - } - return false -} - -func (c *ldapWatcherController) validTLSCondition(message string) *v1alpha1.Condition { - return &v1alpha1.Condition{ - Type: typeTLSConfigurationValid, - Status: v1alpha1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, - Message: message, - } -} - -func (c *ldapWatcherController) invalidTLSCondition(message string) *v1alpha1.Condition { - return &v1alpha1.Condition{ - Type: typeTLSConfigurationValid, - Status: v1alpha1.ConditionFalse, - Reason: upstreamwatchers.ReasonInvalidTLSConfig, - Message: message, - } -} - -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) - if err != nil { - return &v1alpha1.Condition{ - Type: typeBindSecretValid, - Status: v1alpha1.ConditionFalse, - Reason: upstreamwatchers.ReasonNotFound, - Message: err.Error(), - }, "" - } - - if secret.Type != corev1.SecretTypeBasicAuth { - return &v1alpha1.Condition{ - Type: typeBindSecretValid, - Status: v1alpha1.ConditionFalse, - Reason: upstreamwatchers.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: upstreamwatchers.ReasonMissingKeys, - Message: fmt.Sprintf("referenced Secret %q is missing required keys %q", - secretName, []string{corev1.BasicAuthUsernameKey, corev1.BasicAuthPasswordKey}), - }, secret.ResourceVersion - } - - return &v1alpha1.Condition{ - Type: typeBindSecretValid, - Status: v1alpha1.ConditionTrue, - Reason: upstreamwatchers.ReasonSuccess, - Message: "loaded bind secret", - }, secret.ResourceVersion + return upstreamwatchers.EvaluateConditions(conditions, config) } func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) { diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index c801e7419..9d4752920 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -25,6 +25,7 @@ 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/certauthority" + "go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/endpointaddr" "go.pinniped.dev/internal/mocks/mockldapconn" @@ -273,7 +274,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { tests := []struct { name string - initialValidatedSettings map[string]validatedSettings + initialValidatedSettings map[string]upstreamwatchers.ValidatedSettings inputUpstreams []runtime.Object inputSecrets []runtime.Object setupMocks func(conn *mockldapconn.MockConn) @@ -281,7 +282,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { wantErr string wantResultingCache []*upstreamldap.ProviderConfig wantResultingUpstreams []v1alpha1.LDAPIdentityProvider - wantValidatedSettings map[string]validatedSettings + wantValidatedSettings map[string]upstreamwatchers.ValidatedSettings }{ { name: "no LDAPIdentityProvider upstreams clears the cache", @@ -304,7 +305,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "missing secret", @@ -487,7 +488,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when TLS connection fails it tries to use StartTLS instead: without a specified port it automatically switches ports", @@ -545,7 +546,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, }, { name: "when TLS connection fails it tries to use StartTLS instead: with a specified port it does not automatically switch ports", @@ -642,7 +643,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", @@ -685,7 +686,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when testing the connection to the LDAP server fails then the upstream is still added to the cache anyway (treated like a warning)", @@ -729,7 +730,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, 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. }, @@ -741,7 +742,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when the LDAP server connection was already validated using StartTLS for the current resource generation and secret version, then do not validate it again and keep using StartTLS", @@ -752,7 +753,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, 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. }, @@ -764,7 +765,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, }, { name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again", @@ -775,7 +776,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -789,7 +790,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again", @@ -807,7 +808,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "1", LDAPConnectionProtocol: upstreamldap.TLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "1", LDAPConnectionProtocol: upstreamldap.TLS}}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -821,7 +822,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, { 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", @@ -831,8 +832,8 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { ldapConnectionValidTrueCondition(1234, "4241"), // same spec generation, old secret version } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! - initialValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS}}, // old version was validated + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS}}, // old version was validated setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -846,7 +847,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]validatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, }, } @@ -882,7 +883,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { return conn, nil })} - validatedSecretVersionCache := newSecretVersionCache() + validatedSecretVersionCache := upstreamwatchers.NewSecretVersionCache() if tt.initialValidatedSettings != nil { validatedSecretVersionCache.ValidatedSettingsByName = tt.initialValidatedSettings } @@ -936,7 +937,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { // Check that the controller remembered which version of the secret it most recently validated successfully with. if tt.wantValidatedSettings == nil { - tt.wantValidatedSettings = map[string]validatedSettings{} + tt.wantValidatedSettings = map[string]upstreamwatchers.ValidatedSettings{} } require.Equal(t, tt.wantValidatedSettings, validatedSecretVersionCache.ValidatedSettingsByName) }) diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go index 36bd37c83..65b34eea4 100644 --- a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -3,7 +3,22 @@ package upstreamwatchers -import "go.pinniped.dev/internal/constable" +import ( + "context" + "crypto/x509" + "encoding/base64" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + corev1informers "k8s.io/client-go/informers/core/v1" + + "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/oidc/provider" + "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/upstreamldap" +) const ( ReasonNotFound = "SecretNotFound" @@ -13,4 +28,294 @@ const ( ReasonInvalidTLSConfig = "InvalidTLSConfig" ErrNoCertificates = constable.Error("no certificates found") + + LDAPBindAccountSecretType = corev1.SecretTypeBasicAuth + 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" ) + +// An in-memory cache with an entry for each ActiveDirectoryIdentityProvider, to keep track of which ResourceVersion +// of the bind Secret and which TLS/StartTLS setting was used during the most recent successful validation. +type SecretVersionCache struct { + ValidatedSettingsByName map[string]ValidatedSettings +} + +type ValidatedSettings struct { + BindSecretResourceVersion string + LDAPConnectionProtocol upstreamldap.LDAPConnectionProtocol +} + +func NewSecretVersionCache() *SecretVersionCache { + return &SecretVersionCache{ValidatedSettingsByName: map[string]ValidatedSettings{}} +} + +// read only interface for sharing between ldap and active directory. +type UpstreamGenericLDAPIDP interface { + Spec() UpstreamGenericLDAPSpec + Name() string + Namespace() string + Generation() int64 + Status() UpstreamGenericLDAPStatus +} + +type UpstreamGenericLDAPSpec interface { + Host() string + TLSSpec() *v1alpha1.TLSSpec + BindSecretName() string + UserSearch() UpstreamGenericLDAPUserSearch + GroupSearch() UpstreamGenericLDAPGroupSearch +} + +type UpstreamGenericLDAPUserSearch interface { + Base() string + Filter() string + UsernameAttribute() string + UIDAttribute() string +} + +type UpstreamGenericLDAPGroupSearch interface { + Base() string + Filter() string + GroupNameAttribute() string +} + +type UpstreamGenericLDAPStatus interface { + Conditions() []v1alpha1.Condition +} + +func ValidateTLSConfig(tlsSpec *v1alpha1.TLSSpec, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { + if tlsSpec == nil { + return validTLSCondition(noTLSConfigurationMessage) + } + if len(tlsSpec.CertificateAuthorityData) == 0 { + return validTLSCondition(loadedTLSConfigurationMessage) + } + + bundle, err := base64.StdEncoding.DecodeString(tlsSpec.CertificateAuthorityData) + if err != nil { + return invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", err.Error())) + } + + ca := x509.NewCertPool() + ok := ca.AppendCertsFromPEM(bundle) + if !ok { + return invalidTLSCondition(fmt.Sprintf("certificateAuthorityData is invalid: %s", ErrNoCertificates)) + } + + config.CABundle = bundle + return validTLSCondition(loadedTLSConfigurationMessage) +} + +func TestConnection( + ctx context.Context, + bindSecretName string, + config *upstreamldap.ProviderConfig, + currentSecretVersion string, +) *v1alpha1.Condition { + // First try using TLS. + config.ConnectionProtocol = upstreamldap.TLS + tlsLDAPProvider := upstreamldap.New(*config) + err := tlsLDAPProvider.TestConnection(ctx) + if err != nil { + plog.InfoErr("testing LDAP connection using TLS failed, so trying again with StartTLS", err, "host", config.Host) + // If there was any error, try again with StartTLS instead. + config.ConnectionProtocol = upstreamldap.StartTLS + startTLSLDAPProvider := upstreamldap.New(*config) + startTLSErr := startTLSLDAPProvider.TestConnection(ctx) + if startTLSErr == nil { + plog.Info("testing LDAP connection using StartTLS succeeded", "host", config.Host) + // Successfully able to fall back to using StartTLS, so clear the original + // error and consider the connection test to be successful. + err = nil + } else { + plog.InfoErr("testing LDAP connection using StartTLS also failed", err, "host", config.Host) + // Falling back to StartTLS also failed, so put TLS back into the config + // and consider the connection test to be failed. + config.ConnectionProtocol = upstreamldap.TLS + } + } + + 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" [validated with Secret "%s" at version "%s"]`, + config.Host, config.BindUsername, bindSecretName, currentSecretVersion), + } +} + +func HasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(secretVersionCache *SecretVersionCache, currentGeneration int64, upstreamStatusConditions []v1alpha1.Condition, upstreamName string, currentSecretVersion string, config *upstreamldap.ProviderConfig) bool { + for _, cond := range upstreamStatusConditions { + 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, if any. + validatedSecretVersion := secretVersionCache.ValidatedSettingsByName[upstreamName] + if validatedSecretVersion.BindSecretResourceVersion == currentSecretVersion { + // Reload the TLS vs StartTLS setting that was previously validated. + config.ConnectionProtocol = validatedSecretVersion.LDAPConnectionProtocol + return true + } + } + } + return false +} + +func validTLSCondition(message string) *v1alpha1.Condition { + return &v1alpha1.Condition{ + Type: typeTLSConfigurationValid, + Status: v1alpha1.ConditionTrue, + Reason: ReasonSuccess, + Message: message, + } +} + +func invalidTLSCondition(message string) *v1alpha1.Condition { + return &v1alpha1.Condition{ + Type: typeTLSConfigurationValid, + Status: v1alpha1.ConditionFalse, + Reason: ReasonInvalidTLSConfig, + Message: message, + } +} + +func ValidateSecret(secretInformer corev1informers.SecretInformer, secretName string, secretNamespace string, config *upstreamldap.ProviderConfig) (*v1alpha1.Condition, string) { + secret, err := secretInformer.Lister().Secrets(secretNamespace).Get(secretName) + if err != nil { + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + 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), + }, 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}), + }, secret.ResourceVersion + } + + return &v1alpha1.Condition{ + Type: typeBindSecretValid, + Status: v1alpha1.ConditionTrue, + Reason: ReasonSuccess, + Message: "loaded bind secret", + }, secret.ResourceVersion +} + +type GradatedConditions struct { + gradatedConditions []GradatedCondition +} + +func (g *GradatedConditions) Conditions() []*v1alpha1.Condition { + conditions := []*v1alpha1.Condition{} + for _, gc := range g.gradatedConditions { + conditions = append(conditions, gc.condition) + } + return conditions +} + +func (g *GradatedConditions) Append(condition *v1alpha1.Condition, isFatal bool) { + g.gradatedConditions = append(g.gradatedConditions, GradatedCondition{condition: condition, isFatal: isFatal}) +} + +// A condition and a boolean that tells you whether it's fatal or just a warning. +type GradatedCondition struct { + condition *v1alpha1.Condition + isFatal bool +} + +func ValidateGenericLDAP(ctx context.Context, upstream UpstreamGenericLDAPIDP, secretInformer corev1informers.SecretInformer, validatedSecretVersionsCache *SecretVersionCache, config *upstreamldap.ProviderConfig) GradatedConditions { + conditions := GradatedConditions{} + secretValidCondition, currentSecretVersion := ValidateSecret(secretInformer, upstream.Spec().BindSecretName(), upstream.Namespace(), config) + conditions.Append(secretValidCondition, true) + tlsValidCondition := ValidateTLSConfig(upstream.Spec().TLSSpec(), config) + conditions.Append(tlsValidCondition, true) + + // No point in trying to connect to the server if the config was already determined to be invalid. + var ldapConnectionValidCondition *v1alpha1.Condition + if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { + ldapConnectionValidCondition = validateAndSetLDAPServerConnectivity(ctx, validatedSecretVersionsCache, upstream, config, currentSecretVersion) + if ldapConnectionValidCondition != nil { + conditions.Append(ldapConnectionValidCondition, false) + } + } + return conditions +} + +func validateAndSetLDAPServerConnectivity(ctx context.Context, validatedSecretVersionsCache *SecretVersionCache, upstream UpstreamGenericLDAPIDP, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { + // TODO refactor validateAndSetLDAPServerConnectivity to be shared and take a helper function for the defaultNamingContext stuff + // so that can be shared. + if HasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(validatedSecretVersionsCache, upstream.Generation(), upstream.Status().Conditions(), upstream.Name(), currentSecretVersion, config) { + return nil + } + + testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, TestLDAPConnectionTimeout) + defer cancelFunc() + + condition := TestConnection(testConnectionTimeout, upstream.Spec().BindSecretName(), config, 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. + validatedSecretVersionsCache.ValidatedSettingsByName[upstream.Name()] = ValidatedSettings{ + BindSecretResourceVersion: currentSecretVersion, + LDAPConnectionProtocol: config.ConnectionProtocol, + } + } + + return condition +} + +func EvaluateConditions(conditions GradatedConditions, config *upstreamldap.ProviderConfig) (provider.UpstreamLDAPIdentityProviderI, bool) { + for _, gradatedCondition := range conditions.gradatedConditions { + if gradatedCondition.condition.Status != v1alpha1.ConditionTrue && gradatedCondition.isFatal { + // Invalid provider, so do not load it into the cache. + return nil, true + } + } + + for _, gradatedCondition := range conditions.gradatedConditions { + if gradatedCondition.condition.Status != v1alpha1.ConditionTrue && !gradatedCondition.isFatal { + // Error but load it into the cache anyway, treating this condition failure more like a warning. + // Try again hoping that the condition will improve. + return upstreamldap.New(*config), true + } + } + // Fully validated provider, so load it into the cache. + return upstreamldap.New(*config), false +} diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 8b449db88..17c4ec55f 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -343,7 +343,7 @@ func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, ad require.Equal(t, "loaded bind secret", condition.Message) case "TLSConfigurationValid": require.Equal(t, "loaded TLS configuration", condition.Message) - case "ActiveDirectoryConnectionValid": + case "LDAPConnectionValid": require.Equal(t, expectedActiveDirectoryConnectionValidMessage, condition.Message) } } @@ -351,7 +351,7 @@ func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, ad require.ElementsMatch(t, [][]string{ {"BindSecretValid", "True", "Success"}, {"TLSConfigurationValid", "True", "Success"}, - {"ActiveDirectoryConnectionValid", "True", "Success"}, + {"LDAPConnectionValid", "True", "Success"}, }, conditionsSummary) } From 8e1d70562d924c98889fffc878005f78c0a676bc Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Mon, 19 Jul 2021 14:18:29 -0700 Subject: [PATCH 14/36] Remove shared variables from ldap upstream observer --- .../ldapupstreamwatcher/ldap_upstream_watcher.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index 968b82a6d..838f5574c 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -7,9 +7,7 @@ package ldapupstreamwatcher import ( "context" "fmt" - "time" - 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" @@ -28,9 +26,7 @@ import ( ) const ( - ldapControllerName = "ldap-upstream-observer" - ldapBindAccountSecretType = corev1.SecretTypeBasicAuth - testLDAPConnectionTimeout = 90 * time.Second + ldapControllerName = "ldap-upstream-observer" ) type ldapUpstreamGenericLDAPImpl struct { @@ -187,7 +183,7 @@ func newInternal( ), withInformer( secretInformer, - pinnipedcontroller.MatchAnySecretOfTypeFilter(ldapBindAccountSecretType, pinnipedcontroller.SingletonQueue()), + pinnipedcontroller.MatchAnySecretOfTypeFilter(upstreamwatchers.LDAPBindAccountSecretType, pinnipedcontroller.SingletonQueue()), controllerlib.InformerOption{}, ), ) From cb0ee07b5152b9312ad3aa0f5a3ca97aeb8ca501 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 21 Jul 2021 13:24:54 -0700 Subject: [PATCH 15/36] Fetch AD search base from defaultNamingContext when not specified --- .../active_directory_upstream_watcher.go | 39 ++ .../active_directory_upstream_watcher_test.go | 519 +++++++++++++++++- .../ldap_upstream_watcher.go | 6 + .../ldap_upstream_watcher_test.go | 86 ++- .../upstreamwatchers/upstream_watchers.go | 70 ++- internal/upstreamldap/upstreamldap.go | 43 ++ test/integration/supervisor_login_test.go | 13 +- 7 files changed, 712 insertions(+), 64 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 27357bfeb..6f83699ba 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -81,6 +81,45 @@ func (s *activeDirectoryUpstreamGenericLDAPSpec) GroupSearch() upstreamwatchers. return &activeDirectoryUpstreamGenericLDAPGroupSearch{s.activeDirectoryIdentityProvider.Spec.GroupSearch} } +func (s *activeDirectoryUpstreamGenericLDAPSpec) DetectAndSetSearchBase(ctx context.Context, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { + config.GroupSearch.Base = s.activeDirectoryIdentityProvider.Spec.GroupSearch.Base + config.UserSearch.Base = s.activeDirectoryIdentityProvider.Spec.UserSearch.Base + if config.GroupSearch.Base != "" && config.UserSearch.Base != "" { + // Both were already set in spec so just return; no need to query the RootDSE + return &v1alpha1.Condition{ + Type: "SearchBaseFound", + Status: v1alpha1.ConditionTrue, + Reason: "Success", + Message: "Using search base from ActiveDirectoryIdentityProvider config.", + } + } + ldapProvider := upstreamldap.New(*config) + // Query your AD server for the defaultNamingContext to get a DN to use as the search base + // when it isn't specified. + // https://ldapwiki.com/wiki/DefaultNamingContext + defaultNamingContext, err := ldapProvider.SearchForDefaultNamingContext(ctx) + if err != nil { + return &v1alpha1.Condition{ + Type: upstreamwatchers.TypeSearchBaseFound, + Status: v1alpha1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf(`Error finding search base: %s`, err.Error()), + } + } + if config.UserSearch.Base == "" { + config.UserSearch.Base = defaultNamingContext + } + if config.GroupSearch.Base == "" { + config.GroupSearch.Base = defaultNamingContext + } + return &v1alpha1.Condition{ + Type: upstreamwatchers.TypeSearchBaseFound, + Status: v1alpha1.ConditionTrue, + Reason: "Success", + Message: "Successfully fetched defaultNamingContext to use as default search base from RootDSE.", + } +} + type activeDirectoryUpstreamGenericLDAPUserSearch struct { userSearch v1alpha1.ActiveDirectoryIdentityProviderUserSearch } diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index f944c710c..20014656d 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -256,11 +256,51 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { ObservedGeneration: gen, } } + + searchBaseFoundInRootDSECondition := func(gen int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "SearchBaseFound", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "Successfully fetched defaultNamingContext to use as default search base from RootDSE.", + ObservedGeneration: gen, + } + } + + searchBaseFoundInConfigCondition := func(gen int64) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "SearchBaseFound", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "Using search base from ActiveDirectoryIdentityProvider config.", + ObservedGeneration: gen, + } + } + + searchBaseFoundErrorCondition := func(gen int64, message string) v1alpha1.Condition { + return v1alpha1.Condition{ + Type: "SearchBaseFound", + Status: "False", + LastTransitionTime: now, + Reason: "Error", + Message: message, + ObservedGeneration: gen, + } + } + allConditionsTrue := func(gen int64, secretVersion string) []v1alpha1.Condition { return []v1alpha1.Condition{ bindSecretValidTrueCondition(gen), activeDirectoryConnectionValidTrueCondition(gen, secretVersion), + searchBaseFoundInConfigCondition(gen), tlsConfigurationValidLoadedTrueCondition(gen), + // TODO should there be a condition when you just get it from the config? is that worth reporting? + // I'm thinking maybe no since it's not a network call or anything... it's just like any other field in the + // spec that we don't bother to report on. + // Although perhaps it would be weirder to have a condition that only sometimes exists? And it's a useful + // way to communicate internally. } } @@ -272,6 +312,34 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } } + expectedDefaultNamingContextSearch := func() *ldap.SearchRequest { + request := &ldap.SearchRequest{ + BaseDN: "", + Scope: ldap.ScopeBaseObject, + DerefAliases: ldap.NeverDerefAliases, + SizeLimit: 2, + TimeLimit: 90, + TypesOnly: false, + Filter: "(objectClass=*)", + Attributes: []string{"defaultNamingContext"}, + Controls: nil, // don't need paging because we set the SizeLimit so small + } + return request + } + + exampleDefaultNamingContext := "dc=default,dc=naming,dc=context,dc=example,dc=com" + + exampleDefaultNamingContextSearchResult := &ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("defaultNamingContext", []string{exampleDefaultNamingContext}), + }, + }, + }, + } + tests := []struct { name string initialValidatedSettings map[string]upstreamwatchers.ValidatedSettings @@ -305,7 +373,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, }, { name: "missing secret", @@ -477,6 +545,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: []v1alpha1.Condition{ bindSecretValidTrueCondition(1234), activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInConfigCondition(1234), { Type: "TLSConfigurationValid", Status: "True", @@ -488,7 +557,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, }, { name: "when TLS connection fails it tries to use StartTLS instead: without a specified port it automatically switches ports", @@ -542,11 +611,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { "ldap.example.com", testBindUsername, testSecretName, "4242"), ObservedGeneration: 1234, }, + searchBaseFoundInConfigCondition(1234), tlsConfigurationValidLoadedTrueCondition(1234), }, }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, }, { name: "when TLS connection fails it tries to use StartTLS instead: with a specified port it does not automatically switch ports", @@ -599,10 +669,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { "ldap.example.com:5678", testBindUsername, "ldap.example.com:5678"), ObservedGeneration: 1234, }, + searchBaseFoundInConfigCondition(1234), tlsConfigurationValidLoadedTrueCondition(1234), }, }, }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, }, { name: "non-nil TLS configuration with empty CertificateAuthorityData is valid", @@ -643,7 +715,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, }, { name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", @@ -686,7 +758,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, }, }, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, }, { name: "when testing the connection to the LDAP server fails then the upstream is still added to the cache anyway (treated like a warning)", @@ -716,10 +788,107 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { testHost, testBindUsername, testBindUsername), ObservedGeneration: 1234, }, + searchBaseFoundInConfigCondition(1234), tlsConfigurationValidLoadedTrueCondition(1234), }, }, }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, + }, + { + name: "when testing the connection to the LDAP server fails, but later querying defaultsearchbase succeeds, then the upstream is still added to the cache anyway (treated like a warning)", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + // Expect three calls bind: once for trying TLS and once for trying StartTLS (both fail), and one before querying for defaultNamingContext (succeeds) + first := conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2).Return(errors.New("some bind error")) + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1).After(first) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Times(1).Return(exampleDefaultNamingContextSearchResult, nil) + conn.EXPECT().Close().Times(3) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingCache: []*upstreamldap.ProviderConfig{ + { + Name: testName, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: exampleDefaultNamingContext, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(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, + }, + searchBaseFoundInRootDSECondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: testGroupSearchBase}}, + }, + { + name: "when testing the connection to the LDAP server fails, and querying defaultsearchbase fails, then the upstream is not added to the cache", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + // Expect 3 calls to each of these: once for trying TLS and once for trying StartTLS, one before querying + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(3).Return(errors.New("some bind error")) + conn.EXPECT().Close().Times(3) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(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, + }, + searchBaseFoundErrorCondition(1234, "Error finding search base: error binding as \"test-bind-username\" before user search: some bind error"), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {GroupSearchBase: testGroupSearchBase}}, }, { name: "when the LDAP server connection was already validated using TLS for the current resource generation and secret version, then do not validate it again and keep using TLS", @@ -730,7 +899,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { } })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, 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. }, @@ -742,7 +911,112 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, + }, + { + name: "when the LDAP server connection was already validated using TLS, but the search base wasn't, load TLS into the config and try again for the search base", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + } + upstream.Spec.UserSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Close().Times(1) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Return(exampleDefaultNamingContextSearchResult, nil).Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{ + { + Name: testName, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: exampleDefaultNamingContext, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInRootDSECondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: testGroupSearchBase}}, + }, + { + name: "when the LDAP server connection was already validated using TLS, and the search base was found, load TLS and search base info into the cache", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Generation = 1234 + upstream.Status.Conditions = []v1alpha1.Condition{ + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInRootDSECondition(1234), + } + upstream.Spec.UserSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: testGroupSearchBase}}, + setupMocks: func(conn *mockldapconn.MockConn) { + }, + wantResultingCache: []*upstreamldap.ProviderConfig{ + { + Name: testName, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: exampleDefaultNamingContext, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInRootDSECondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: exampleDefaultNamingContext, + GroupSearchBase: testGroupSearchBase, + }}, }, { name: "when the LDAP server connection was already validated using StartTLS for the current resource generation and secret version, then do not validate it again and keep using StartTLS", @@ -765,7 +1039,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.StartTLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, }, { name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again", @@ -775,8 +1054,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { activeDirectoryConnectionValidTrueCondition(1233, "4242"), // older spec generation! } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -790,7 +1074,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, }, { name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again", @@ -822,7 +1111,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, }, { 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", @@ -832,8 +1126,13 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { activeDirectoryConnectionValidTrueCondition(1234, "4241"), // same spec generation, old secret version } })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! - initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4241", LDAPConnectionProtocol: upstreamldap.TLS}}, // old version was validated + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! + initialValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4241", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, // old version was validated setupMocks: func(conn *mockldapconn.MockConn) { // Should perform a test dial and bind. conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -847,7 +1146,12 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{ + testName: {BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, }, { name: "when the input activedirectoryidentityprovider leaves user attributes blank, provide default values", @@ -888,7 +1192,190 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, + }, + { + name: "when the input activedirectoryidentityprovider leaves user and group search base blank, query for defaultNamingContext", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.UserSearch.Base = "" + upstream.Spec.GroupSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) + conn.EXPECT().Close().Times(2) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Return(exampleDefaultNamingContextSearchResult, nil).Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{ + { + Name: testName, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: exampleDefaultNamingContext, + Filter: testUserSearchFilter, + UsernameAttribute: "sAMAccountName", + UIDAttribute: "objectGUID", + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: exampleDefaultNamingContext, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInRootDSECondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: exampleDefaultNamingContext}}, + }, + { + name: "when the input activedirectoryidentityprovider leaves user search base blank but provides group search base, query for defaultNamingContext", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.UserSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) + conn.EXPECT().Close().Times(2) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Return(exampleDefaultNamingContextSearchResult, nil).Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{ + { + Name: testName, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: exampleDefaultNamingContext, + Filter: testUserSearchFilter, + UsernameAttribute: "sAMAccountName", + UIDAttribute: "objectGUID", + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInRootDSECondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: exampleDefaultNamingContext, GroupSearchBase: testGroupSearchBase}}, + }, + { + name: "when the input activedirectoryidentityprovider leaves group search base blank but provides user search base, query for defaultNamingContext", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.GroupSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) + conn.EXPECT().Close().Times(2) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Return(exampleDefaultNamingContextSearchResult, nil).Times(1) + }, + wantResultingCache: []*upstreamldap.ProviderConfig{ + { + Name: testName, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: "sAMAccountName", + UIDAttribute: "objectGUID", + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: exampleDefaultNamingContext, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInRootDSECondition(1234), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: exampleDefaultNamingContext}}, + }, + { + name: "when the input activedirectoryidentityprovider leaves group search base blank and query for defaultNamingContext fails", + // TODO is this a fatal error? I think so because leaving the search base blank and trying anyway does not seem expected. + // it could potentially succeed but return something unexpected... + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.GroupSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) + conn.EXPECT().Close().Times(2) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Return(nil, errors.New("some error")).Times(1) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundErrorCondition(1234, "Error finding search base: error querying RootDSE for defaultNamingContext: some error"), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{ + testName: {BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase}}, }, } diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index 838f5574c..90d172b1a 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -77,6 +77,12 @@ func (s *ldapUpstreamGenericLDAPSpec) GroupSearch() upstreamwatchers.UpstreamGen return &ldapUpstreamGenericLDAPGroupSearch{s.ldapIdentityProvider.Spec.GroupSearch} } +func (s *ldapUpstreamGenericLDAPSpec) DetectAndSetSearchBase(_ context.Context, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { + config.GroupSearch.Base = s.ldapIdentityProvider.Spec.GroupSearch.Base + config.UserSearch.Base = s.ldapIdentityProvider.Spec.UserSearch.Base + return nil +} + type ldapUpstreamGenericLDAPUserSearch struct { userSearch v1alpha1.LDAPIdentityProviderUserSearch } diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index 9d4752920..169936402 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -305,7 +305,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, }, { name: "missing secret", @@ -488,8 +493,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, - }, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}}, { name: "when TLS connection fails it tries to use StartTLS instead: without a specified port it automatically switches ports", inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { @@ -546,8 +555,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, - }, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.StartTLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}}, { name: "when TLS connection fails it tries to use StartTLS instead: with a specified port it does not automatically switch ports", inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { @@ -603,6 +616,10 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, }, { name: "non-nil TLS configuration with empty CertificateAuthorityData is valid", @@ -643,7 +660,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, }, { name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", @@ -686,8 +708,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, - }, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}}, { 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}, @@ -720,6 +746,10 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { }, }, }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, }, { name: "when the LDAP server connection was already validated using TLS for the current resource generation and secret version, then do not validate it again and keep using TLS", @@ -742,8 +772,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, - }, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}}, { name: "when the LDAP server connection was already validated using StartTLS for the current resource generation and secret version, then do not validate it again and keep using StartTLS", inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.LDAPIdentityProvider) { @@ -765,8 +799,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.StartTLS}}, - }, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.StartTLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}}, { 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) { @@ -790,8 +828,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, - }, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}}, { 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) { @@ -822,8 +864,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, - }, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}}, { 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) { @@ -847,8 +893,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { Conditions: allConditionsTrue(1234, "4242"), }, }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS}}, - }, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}}, } for _, tt := range tests { diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go index 65b34eea4..58522d60a 100644 --- a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -36,13 +36,14 @@ const ( typeBindSecretValid = "BindSecretValid" typeTLSConfigurationValid = "TLSConfigurationValid" typeLDAPConnectionValid = "LDAPConnectionValid" + TypeSearchBaseFound = "SearchBaseFound" reasonLDAPConnectionError = "LDAPConnectionError" noTLSConfigurationMessage = "no TLS configuration provided" loadedTLSConfigurationMessage = "loaded TLS configuration" ) // An in-memory cache with an entry for each ActiveDirectoryIdentityProvider, to keep track of which ResourceVersion -// of the bind Secret and which TLS/StartTLS setting was used during the most recent successful validation. +// of the bind Secret, which TLS/StartTLS setting was used and which search base was found during the most recent successful validation. type SecretVersionCache struct { ValidatedSettingsByName map[string]ValidatedSettings } @@ -50,6 +51,8 @@ type SecretVersionCache struct { type ValidatedSettings struct { BindSecretResourceVersion string LDAPConnectionProtocol upstreamldap.LDAPConnectionProtocol + UserSearchBase string + GroupSearchBase string } func NewSecretVersionCache() *SecretVersionCache { @@ -71,6 +74,7 @@ type UpstreamGenericLDAPSpec interface { BindSecretName() string UserSearch() UpstreamGenericLDAPUserSearch GroupSearch() UpstreamGenericLDAPGroupSearch + DetectAndSetSearchBase(ctx context.Context, config *upstreamldap.ProviderConfig) *v1alpha1.Condition } type UpstreamGenericLDAPUserSearch interface { @@ -161,7 +165,7 @@ func TestConnection( } } -func HasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(secretVersionCache *SecretVersionCache, currentGeneration int64, upstreamStatusConditions []v1alpha1.Condition, upstreamName string, currentSecretVersion string, config *upstreamldap.ProviderConfig) bool { +func HasPreviousSuccessfulTLSConnectionConditionForCurrentSpecGenerationAndSecretVersion(secretVersionCache *SecretVersionCache, currentGeneration int64, upstreamStatusConditions []v1alpha1.Condition, upstreamName string, currentSecretVersion string, config *upstreamldap.ProviderConfig) bool { for _, cond := range upstreamStatusConditions { if cond.Type == typeLDAPConnectionValid && cond.Status == v1alpha1.ConditionTrue && cond.ObservedGeneration == currentGeneration { // Found a previously successful condition for the current spec generation. @@ -177,6 +181,21 @@ func HasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(secr return false } +func HasPreviousSuccessfulSearchBaseConditionForCurrentGeneration(secretVersionCache *SecretVersionCache, currentGeneration int64, upstreamStatusConditions []v1alpha1.Condition, upstreamName string, currentSecretVersion string, config *upstreamldap.ProviderConfig) bool { + for _, cond := range upstreamStatusConditions { + if cond.Type == TypeSearchBaseFound && 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, if any. + validatedSettings := secretVersionCache.ValidatedSettingsByName[upstreamName] + // Reload the TLS vs StartTLS setting that was previously validated. + config.UserSearch.Base = validatedSettings.UserSearchBase + config.GroupSearch.Base = validatedSettings.GroupSearchBase + return true + } + } + return false +} + func validTLSCondition(message string) *v1alpha1.Condition { return &v1alpha1.Condition{ Type: typeTLSConfigurationValid, @@ -267,38 +286,47 @@ func ValidateGenericLDAP(ctx context.Context, upstream UpstreamGenericLDAPIDP, s // No point in trying to connect to the server if the config was already determined to be invalid. var ldapConnectionValidCondition *v1alpha1.Condition + var searchBaseFoundCondition *v1alpha1.Condition if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { - ldapConnectionValidCondition = validateAndSetLDAPServerConnectivity(ctx, validatedSecretVersionsCache, upstream, config, currentSecretVersion) + ldapConnectionValidCondition, searchBaseFoundCondition = validateAndSetLDAPServerConnectivity(ctx, validatedSecretVersionsCache, upstream, config, currentSecretVersion) if ldapConnectionValidCondition != nil { conditions.Append(ldapConnectionValidCondition, false) } + if searchBaseFoundCondition != nil { + conditions.Append(searchBaseFoundCondition, true) + } } return conditions } -func validateAndSetLDAPServerConnectivity(ctx context.Context, validatedSecretVersionsCache *SecretVersionCache, upstream UpstreamGenericLDAPIDP, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { - // TODO refactor validateAndSetLDAPServerConnectivity to be shared and take a helper function for the defaultNamingContext stuff - // so that can be shared. - if HasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(validatedSecretVersionsCache, upstream.Generation(), upstream.Status().Conditions(), upstream.Name(), currentSecretVersion, config) { - return nil - } +func validateAndSetLDAPServerConnectivity(ctx context.Context, validatedSecretVersionsCache *SecretVersionCache, upstream UpstreamGenericLDAPIDP, config *upstreamldap.ProviderConfig, currentSecretVersion string) (*v1alpha1.Condition, *v1alpha1.Condition) { + var ldapConnectionValidCondition *v1alpha1.Condition + if !HasPreviousSuccessfulTLSConnectionConditionForCurrentSpecGenerationAndSecretVersion(validatedSecretVersionsCache, upstream.Generation(), upstream.Status().Conditions(), upstream.Name(), currentSecretVersion, config) { + testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, TestLDAPConnectionTimeout) + defer cancelFunc() - testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, TestLDAPConnectionTimeout) - defer cancelFunc() + ldapConnectionValidCondition = TestConnection(testConnectionTimeout, upstream.Spec().BindSecretName(), config, currentSecretVersion) - condition := TestConnection(testConnectionTimeout, upstream.Spec().BindSecretName(), config, 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. - validatedSecretVersionsCache.ValidatedSettingsByName[upstream.Name()] = ValidatedSettings{ - BindSecretResourceVersion: currentSecretVersion, - LDAPConnectionProtocol: config.ConnectionProtocol, + if ldapConnectionValidCondition.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. + validatedSecretVersionsCache.ValidatedSettingsByName[upstream.Name()] = ValidatedSettings{ + BindSecretResourceVersion: currentSecretVersion, + LDAPConnectionProtocol: config.ConnectionProtocol, + } } } + var searchBaseFoundCondition *v1alpha1.Condition + if !HasPreviousSuccessfulSearchBaseConditionForCurrentGeneration(validatedSecretVersionsCache, upstream.Generation(), upstream.Status().Conditions(), upstream.Name(), currentSecretVersion, config) { + searchBaseFoundCondition = upstream.Spec().DetectAndSetSearchBase(ctx, config) + validatedSettings := validatedSecretVersionsCache.ValidatedSettingsByName[upstream.Name()] + validatedSettings.GroupSearchBase = config.GroupSearch.Base + validatedSettings.UserSearchBase = config.UserSearch.Base + validatedSecretVersionsCache.ValidatedSettingsByName[upstream.Name()] = validatedSettings + } - return condition + return ldapConnectionValidCondition, searchBaseFoundCondition } func EvaluateConditions(conditions GradatedConditions, config *upstreamldap.ProviderConfig) (provider.UpstreamLDAPIdentityProviderI, bool) { diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index e58e76b46..f6bee32fc 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -392,6 +392,35 @@ func (p *Provider) validateConfig() error { return nil } +func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, 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 + + conn, err := p.dial(ctx) + if err != nil { + p.traceAuthFailure(t, err) + 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 { + p.traceAuthFailure(t, err) + return "", fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err) + } + + searchResult, err := conn.Search(p.defaultNamingContextRequest()) + if err != nil { + return "", fmt.Errorf(`error querying RootDSE for defaultNamingContext: %w`, err) + } + // TODO handle getting empty entry back-- I think this is possible but we might want to + // treat it as an error + // TODO handle getting no entries back + // TODO handle getting more than 1 result back + // TODO handle getting no values for defaultNamingContext attribute back in entry + return searchResult.Entries[0].GetAttributeValue("defaultNamingContext"), nil +} + func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, []string, error) { searchResult, err := conn.Search(p.userSearchRequest(username)) if err != nil { @@ -462,6 +491,20 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c return mappedUsername, mappedUID, mappedGroupNames, nil } +func (p *Provider) defaultNamingContextRequest() *ldap.SearchRequest { + return &ldap.SearchRequest{ + BaseDN: "", + Scope: ldap.ScopeBaseObject, + DerefAliases: ldap.NeverDerefAliases, + SizeLimit: 2, + TimeLimit: 90, + TypesOnly: false, + Filter: "(objectClass=*)", + Attributes: []string{"defaultNamingContext"}, + Controls: nil, // don't need paging because we set the SizeLimit so small + } +} + 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{ diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 17c4ec55f..6a556a29d 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -254,12 +254,6 @@ func TestSupervisorLogin(t *testing.T) { TLS: &idpv1alpha1.TLSSpec{ CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), }, - UserSearch: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearch{ - Base: "dc=activedirectory,dc=test,dc=pinniped,dc=dev", - }, - GroupSearch: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearch{ - Base: "dc=activedirectory,dc=test,dc=pinniped,dc=dev", - }, Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ SecretName: secret.Name, }, @@ -269,7 +263,7 @@ func TestSupervisorLogin(t *testing.T) { env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, secret.Name, secret.ResourceVersion, ) - requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) // TODO refactor to be same as LDAP func + requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) }, requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorizationUsingLDAPIdentityProvider(t, @@ -282,7 +276,7 @@ func TestSupervisorLogin(t *testing.T) { // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( "ldaps://" + env.SupervisorUpstreamActiveDirectory.Host + - "?base=" + url.QueryEscape("dc=activedirectory,dc=test,dc=pinniped,dc=dev") + + "?base=" + url.QueryEscape("DC=activedirectory,DC=test,DC=pinniped,DC=dev") + "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute @@ -331,7 +325,7 @@ func requireSuccessfulLDAPIdentityProviderConditions(t *testing.T, ldapIDP *idpv }, conditionsSummary) } func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider, expectedActiveDirectoryConnectionValidMessage string) { - require.Len(t, adIDP.Status.Conditions, 3) + require.Len(t, adIDP.Status.Conditions, 4) conditionsSummary := [][]string{} for _, condition := range adIDP.Status.Conditions { @@ -352,6 +346,7 @@ func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, ad {"BindSecretValid", "True", "Success"}, {"TLSConfigurationValid", "True", "Success"}, {"LDAPConnectionValid", "True", "Success"}, + {"SearchBaseFound", "True", "Success"}, }, conditionsSummary) } From 890d9c3216cb2428e9afec2d9c33b196795d208d Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 21 Jul 2021 15:02:59 -0700 Subject: [PATCH 16/36] resolve some todos about error handling search base discovery results --- .../active_directory_upstream_watcher_test.go | 119 +++++++++++++++++- internal/upstreamldap/upstreamldap.go | 16 ++- 2 files changed, 128 insertions(+), 7 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 20014656d..bde2f91c5 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -1347,7 +1347,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { { name: "when the input activedirectoryidentityprovider leaves group search base blank and query for defaultNamingContext fails", // TODO is this a fatal error? I think so because leaving the search base blank and trying anyway does not seem expected. - // it could potentially succeed but return something unexpected... + // queries with an empty search base could potentially succeed but return something unexpected, like if you were + // pointing at global catalog but not intending to use the GC functionality... inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" @@ -1377,6 +1378,122 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase}}, }, + { + name: "when query for defaultNamingContext returns empty string", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.GroupSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) + conn.EXPECT().Close().Times(2) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("defaultNamingContext", []string{""}), + }, + }, + }}, nil).Times(1) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundErrorCondition(1234, "Error finding search base: error querying RootDSE for defaultNamingContext: empty search base DN found"), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{ + testName: {BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase}}, + }, + { + name: "when query for defaultNamingContext returns multiple entries", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.GroupSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) + conn.EXPECT().Close().Times(2) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("defaultNamingContext", []string{""}), + }, + }, + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("defaultNamingContext", []string{""}), + }, + }, + }}, nil).Times(1) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundErrorCondition(1234, "Error finding search base: error querying RootDSE for defaultNamingContext: expected to find 1 entry but found 2"), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{ + testName: {BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase}}, + }, + { + name: "when query for defaultNamingContext returns no entries", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.GroupSearch.Base = "" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + setupMocks: func(conn *mockldapconn.MockConn) { + // Should perform a test dial and bind. + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(2) + conn.EXPECT().Close().Times(2) + conn.EXPECT().Search(expectedDefaultNamingContextSearch()).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{}}, nil).Times(1) + }, + wantErr: controllerlib.ErrSyntheticRequeue.Error(), + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Error", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundErrorCondition(1234, "Error finding search base: error querying RootDSE for defaultNamingContext: expected to find 1 entry but found 0"), + tlsConfigurationValidLoadedTrueCondition(1234), + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{ + testName: {BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase}}, + }, } for _, tt := range tests { diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index f6bee32fc..443e23b89 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -413,12 +413,16 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e if err != nil { return "", fmt.Errorf(`error querying RootDSE for defaultNamingContext: %w`, err) } - // TODO handle getting empty entry back-- I think this is possible but we might want to - // treat it as an error - // TODO handle getting no entries back - // TODO handle getting more than 1 result back - // TODO handle getting no values for defaultNamingContext attribute back in entry - return searchResult.Entries[0].GetAttributeValue("defaultNamingContext"), nil + + if len(searchResult.Entries) != 1 { + return "", fmt.Errorf(`error querying RootDSE for defaultNamingContext: expected to find 1 entry but found %d`, len(searchResult.Entries)) + } + searchBase := searchResult.Entries[0].GetAttributeValue("defaultNamingContext") + if searchBase == "" { + // if we get an empty search base back, treat it like an error. Otherwise we might make too broad of a search. + return "", fmt.Errorf(`error querying RootDSE for defaultNamingContext: empty search base DN found`) + } + return searchBase, nil } func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, []string, error) { From f99f7be836e18008b590f8e4bf5be51b6b970846 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 21 Jul 2021 16:03:06 -0700 Subject: [PATCH 17/36] Default values for ad usersearch and groupsearch --- .../active_directory_upstream_watcher.go | 38 ++++++++++++++++--- .../active_directory_upstream_watcher_test.go | 9 +++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 6f83699ba..46280b21e 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -29,8 +29,21 @@ const ( activeDirectoryControllerName = "active-directory-upstream-observer" // Default values for active directory config. - defaultActiveDirectoryUsernameAttributeName = "sAMAccountName" - defaultActiveDirectoryUIDAttributeName = "objectGUID" + defaultActiveDirectoryUsernameAttributeName = "sAMAccountName" + defaultActiveDirectoryUIDAttributeName = "objectGUID" + defaultActiveDirectoryGroupNameAttributeName = "sAMAccountName" + + // - is a person. + // - is not a computer. + // - is not shown in advanced view only (which would likely mean its a system created service account with advanced permissions). + // - either the sAMAccountName or the mail attribute matches the input username. + // - the sAMAccountType is for a normal user account. + defaultActiveDirectoryUserSearchFilter = "(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={})(mail={}))(sAMAccountType=805306368))" + + // - is a group. + // - has a member that matches the DN of the user we successfully logged in as. + // - perform nested group search by default. + defaultActiveDirectoryGroupSearchFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})" ) type activeDirectoryUpstreamGenericLDAPImpl struct { @@ -271,19 +284,34 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, uidAttribute = defaultActiveDirectoryUIDAttributeName } + groupNameAttribute := spec.GroupSearch.Attributes.GroupName + if len(groupNameAttribute) == 0 { + groupNameAttribute = defaultActiveDirectoryGroupNameAttributeName + } + + userSearchFilter := spec.UserSearch.Filter + if len(userSearchFilter) == 0 { + userSearchFilter = defaultActiveDirectoryUserSearchFilter + } + + groupSearchFilter := spec.GroupSearch.Filter + if len(groupSearchFilter) == 0 { + groupSearchFilter = defaultActiveDirectoryGroupSearchFilter + } + config := &upstreamldap.ProviderConfig{ Name: upstream.Name, Host: spec.Host, UserSearch: upstreamldap.UserSearchConfig{ Base: spec.UserSearch.Base, - Filter: spec.UserSearch.Filter, + Filter: userSearchFilter, UsernameAttribute: usernameAttribute, UIDAttribute: uidAttribute, }, GroupSearch: upstreamldap.GroupSearchConfig{ Base: spec.GroupSearch.Base, - Filter: spec.GroupSearch.Filter, - GroupNameAttribute: spec.GroupSearch.Attributes.GroupName, + Filter: groupSearchFilter, + GroupNameAttribute: groupNameAttribute, }, Dialer: c.ldapDialer, } diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index bde2f91c5..d05de468c 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -1157,6 +1157,9 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { name: "when the input activedirectoryidentityprovider leaves user attributes blank, provide default values", inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.UserSearch.Filter = "" + upstream.Spec.GroupSearch.Filter = "" + upstream.Spec.GroupSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{} })}, inputSecrets: []runtime.Object{validBindUserSecret("4242")}, setupMocks: func(conn *mockldapconn.MockConn) { @@ -1174,14 +1177,14 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { BindPassword: testBindPassword, UserSearch: upstreamldap.UserSearchConfig{ Base: testUserSearchBase, - Filter: testUserSearchFilter, + Filter: "(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={})(mail={}))(sAMAccountType=805306368))", UsernameAttribute: "sAMAccountName", UIDAttribute: "objectGUID", }, GroupSearch: upstreamldap.GroupSearchConfig{ Base: testGroupSearchBase, - Filter: testGroupSearchFilter, - GroupNameAttribute: testGroupNameAttrName, + Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})", + GroupNameAttribute: "sAMAccountName", }, }, }, From 91085e68f98ec2471a1af8913e45cfe78fc5be21 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 21 Jul 2021 17:06:52 -0700 Subject: [PATCH 18/36] Refactoring defaulting logic --- hack/prepare-for-integration-tests.sh | 1 + .../active_directory_upstream_watcher.go | 53 ++++++++--------- .../active_directory_upstream_watcher_test.go | 2 +- test/integration/supervisor_login_test.go | 2 +- test/testlib/env.go | 57 ++++++++++--------- 5 files changed, 56 insertions(+), 59 deletions(-) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index bdc9ec425..65328e546 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -392,6 +392,7 @@ export PINNIPED_TEST_AD_USER_PASSWORD="$(gcloud secrets versions access latest - export PINNIPED_TEST_AD_LDAPS_CA_BUNDLE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-ca-data' -)" export PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-expected-direct-groups-dn' -)" export PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-expected-direct-groups-cn' -)" +export PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-expected-direct-and-nested-groups-samaccountnames' -)" fi read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 46280b21e..c6dcd0e77 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -43,7 +43,7 @@ const ( // - is a group. // - has a member that matches the DN of the user we successfully logged in as. // - perform nested group search by default. - defaultActiveDirectoryGroupSearchFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})" + defaultActiveDirectoryGroupSearchFilter = "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))" ) type activeDirectoryUpstreamGenericLDAPImpl struct { @@ -142,14 +142,23 @@ func (u *activeDirectoryUpstreamGenericLDAPUserSearch) Base() string { } func (u *activeDirectoryUpstreamGenericLDAPUserSearch) Filter() string { + if len(u.userSearch.Filter) == 0 { + return defaultActiveDirectoryUserSearchFilter + } return u.userSearch.Filter } func (u *activeDirectoryUpstreamGenericLDAPUserSearch) UsernameAttribute() string { + if len(u.userSearch.Attributes.Username) == 0 { + return defaultActiveDirectoryUsernameAttributeName + } return u.userSearch.Attributes.Username } func (u *activeDirectoryUpstreamGenericLDAPUserSearch) UIDAttribute() string { + if len(u.userSearch.Attributes.UID) == 0 { + return defaultActiveDirectoryUIDAttributeName + } return u.userSearch.Attributes.UID } @@ -162,10 +171,16 @@ func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) Base() string { } func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) Filter() string { + if len(g.groupSearch.Filter) == 0 { + return defaultActiveDirectoryGroupSearchFilter + } return g.groupSearch.Filter } func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) GroupNameAttribute() string { + if len(g.groupSearch.Attributes.GroupName) == 0 { + return defaultActiveDirectoryGroupNameAttributeName + } return g.groupSearch.Attributes.GroupName } @@ -275,48 +290,26 @@ func (c *activeDirectoryWatcherController) Sync(ctx controllerlib.Context) error func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) { spec := upstream.Spec - usernameAttribute := spec.UserSearch.Attributes.Username - if len(usernameAttribute) == 0 { - usernameAttribute = defaultActiveDirectoryUsernameAttributeName - } - uidAttribute := spec.UserSearch.Attributes.UID - if len(uidAttribute) == 0 { - uidAttribute = defaultActiveDirectoryUIDAttributeName - } - - groupNameAttribute := spec.GroupSearch.Attributes.GroupName - if len(groupNameAttribute) == 0 { - groupNameAttribute = defaultActiveDirectoryGroupNameAttributeName - } - - userSearchFilter := spec.UserSearch.Filter - if len(userSearchFilter) == 0 { - userSearchFilter = defaultActiveDirectoryUserSearchFilter - } - - groupSearchFilter := spec.GroupSearch.Filter - if len(groupSearchFilter) == 0 { - groupSearchFilter = defaultActiveDirectoryGroupSearchFilter - } + adUpstreamImpl := activeDirectoryUpstreamGenericLDAPImpl{*upstream} config := &upstreamldap.ProviderConfig{ Name: upstream.Name, Host: spec.Host, UserSearch: upstreamldap.UserSearchConfig{ Base: spec.UserSearch.Base, - Filter: userSearchFilter, - UsernameAttribute: usernameAttribute, - UIDAttribute: uidAttribute, + Filter: adUpstreamImpl.Spec().UserSearch().Filter(), + UsernameAttribute: adUpstreamImpl.Spec().UserSearch().UsernameAttribute(), + UIDAttribute: adUpstreamImpl.Spec().UserSearch().UIDAttribute(), }, GroupSearch: upstreamldap.GroupSearchConfig{ Base: spec.GroupSearch.Base, - Filter: groupSearchFilter, - GroupNameAttribute: groupNameAttribute, + Filter: adUpstreamImpl.Spec().GroupSearch().Filter(), + GroupNameAttribute: adUpstreamImpl.Spec().GroupSearch().GroupNameAttribute(), }, Dialer: c.ldapDialer, } - conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &activeDirectoryUpstreamGenericLDAPImpl{*upstream}, c.secretInformer, c.validatedSecretVersionsCache, config) + conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config) c.updateStatus(ctx, upstream, conditions.Conditions()) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index d05de468c..6d11d0cb3 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -1183,7 +1183,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, GroupSearch: upstreamldap.GroupSearchConfig{ Base: testGroupSearchBase, - Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})", + Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))", GroupNameAttribute: "sAMAccountName", }, }, diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 6a556a29d..2da945b26 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -281,7 +281,7 @@ func TestSupervisorLogin(t *testing.T) { ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue), - wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountNames, }, } for _, test := range tests { diff --git a/test/testlib/env.go b/test/testlib/env.go index 04c770e01..ee83e1c78 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -82,23 +82,24 @@ type TestOIDCUpstream struct { } type TestLDAPUpstream struct { - Host string `json:"host"` - StartTLSOnlyHost string `json:"startTLSOnlyHost"` - CABundle string `json:"caBundle"` - BindUsername string `json:"bindUsername"` - BindPassword string `json:"bindPassword"` - UserSearchBase string `json:"userSearchBase"` - GroupSearchBase string `json:"groupSearchBase"` - 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"` - TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` - TestUserDirectGroupsDNs []string `json:"testUserDirectGroupsDNs"` //nolint:golint // this is "distinguished names", not "DNS" - TestUserSAMAccountNameValue string `json:"testUserSAMAccountNameValue"` + Host string `json:"host"` + StartTLSOnlyHost string `json:"startTLSOnlyHost"` + CABundle string `json:"caBundle"` + BindUsername string `json:"bindUsername"` + BindPassword string `json:"bindPassword"` + UserSearchBase string `json:"userSearchBase"` + GroupSearchBase string `json:"groupSearchBase"` + 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"` + TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` + TestUserDirectGroupsDNs []string `json:"testUserDirectGroupsDNs"` //nolint:golint // this is "distinguished names", not "DNS" + TestUserSAMAccountNameValue string `json:"testUserSAMAccountNameValue"` + TestUserIndirectGroupsSAMAccountNames []string `json:"TestUserIndirectGroupsSAMAccountNames"` } // ProxyEnv returns a set of environment variable strings (e.g., to combine with os.Environ()) which set up the configured test HTTP proxy. @@ -270,22 +271,24 @@ func loadEnvVars(t *testing.T, result *TestEnv) { } result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{ - Host: wantEnv("PINNIPED_TEST_AD_HOST", ""), - CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")), - BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""), - BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""), - TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), - TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), - TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), - TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), - TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), - TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), + Host: wantEnv("PINNIPED_TEST_AD_HOST", ""), + CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")), + BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""), + BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""), + TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), + TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), + TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), + TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), + TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), + TestUserIndirectGroupsSAMAccountNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME", ""), ";")), } sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs) sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs) + sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountNames) } func (e *TestEnv) HasCapability(cap Capability) bool { From 8ea1bd3dfb7773aa789f365a5889f81bfd9e12fb Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 22 Jul 2021 10:13:38 -0700 Subject: [PATCH 19/36] Make prepare-for-integration-tests active directory setup accessible for anyone --- hack/prepare-for-integration-tests.sh | 38 +++++++++++---------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index 65328e546..30399f003 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -50,7 +50,7 @@ skip_build=no clean_kind=no api_group_suffix="pinniped.dev" # same default as in the values.yaml ytt file skip_chromedriver_check=no -test_active_directory=no +get_active_directory_vars="" # specify a filename for a script to get AD related env variables while (("$#")); do case "$1" in @@ -80,8 +80,14 @@ while (("$#")); do skip_chromedriver_check=yes shift ;; - --test-active-directory) - test_active_directory=yes + --get-active-directory-vars) + shift + # If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error + if [[ "$#" == "0" || "$1" == -* ]]; then + log_error "-g|--get-active-directory-vars requires a script name to be specified" + exit 1 + fi + get_active_directory_vars=$1 shift ;; -*) @@ -374,25 +380,13 @@ export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD=${dex_test_password} export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_EXPECTED_GROUPS= # Dex's local user store does not let us configure groups. export PINNIPED_TEST_API_GROUP_SUFFIX='${api_group_suffix}' -if [[ "$test_active_directory" == "yes" ]]; then - -if [[ -z "$(gcloud config list account --format "value(core.account)")" ]]; then - echo "Please run \`gcloud auth login\`" - exit 1 -fi - -export PINNIPED_TEST_AD_HOST="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-host' -)" -export PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-bind-account-username' -)" -export PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-bind-account-password' -)" -export PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME="objectGUID" -export PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-unique-id-attribute-value' -)" -export PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_NAME="sAMAccountName" -export PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-sAMAccountName' -)" -export PINNIPED_TEST_AD_USER_PASSWORD="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-user-password' -)" -export PINNIPED_TEST_AD_LDAPS_CA_BUNDLE="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-ca-data' -)" -export PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-expected-direct-groups-dn' -)" -export PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-expected-direct-groups-cn' -)" -export PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME="$(gcloud secrets versions access latest --secret="concourse-secrets" --project tanzu-user-authentication | yq e '.aws-ad-expected-direct-and-nested-groups-samaccountnames' -)" +# We can't set up an in-cluster active directory instance, but +# if you have an active directory instance that you wish to run the tests against, +# specify a script to set the ad-related environment variables. +# You will need to set the environment variables that start with "PINNIPED_TEST_AD_" +# found in pinniped/test/testlib/env.go. +if [[ "$get_active_directory_vars" != "" ]]; then + source $get_active_directory_vars fi read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true From 00978c15f78056063d3c23783444be0d80056d9f Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 22 Jul 2021 10:41:24 -0700 Subject: [PATCH 20/36] Update wording for ActiveDirectoryIdentityProvider crd --- ...es_activedirectoryidentityprovider.go.tmpl | 26 +++++----- ....dev_activedirectoryidentityproviders.yaml | 49 ++++++++----------- generated/1.17/README.adoc | 12 ++--- .../types_activedirectoryidentityprovider.go | 26 +++++----- ....dev_activedirectoryidentityproviders.yaml | 49 ++++++++----------- generated/1.18/README.adoc | 12 ++--- .../types_activedirectoryidentityprovider.go | 26 +++++----- ....dev_activedirectoryidentityproviders.yaml | 49 ++++++++----------- generated/1.19/README.adoc | 12 ++--- .../types_activedirectoryidentityprovider.go | 26 +++++----- ....dev_activedirectoryidentityproviders.yaml | 49 ++++++++----------- generated/1.20/README.adoc | 12 ++--- .../types_activedirectoryidentityprovider.go | 26 +++++----- ....dev_activedirectoryidentityproviders.yaml | 49 ++++++++----------- .../types_activedirectoryidentityprovider.go | 26 +++++----- go.mod | 1 - go.sum | 1 - 17 files changed, 201 insertions(+), 250 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl index dc108466c..791e47a6d 100644 --- a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl @@ -46,7 +46,7 @@ type ActiveDirectoryIdentityProviderBind struct { } type ActiveDirectoryIdentityProviderUserSearchAttributes struct { - // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // Optional, when empty this defaults to "sAMAccountName". // +optional @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // Optional. When not specified, this defaults to "sAMAccountName". // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,20 +72,17 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will search the whole directory tree. - // Note that if your bind user only has permission to search a subtree, this must be specified. - // Search a subtree will also be faster. + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` - // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter at least once 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 ActiveDirectory filters, see + // 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. + // Optional. When not specified, the default will be + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -97,9 +94,8 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. - // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and - // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, - // the values of Filter and Attributes are ignored. + // "ou=groups,dc=example,dc=com". + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` @@ -109,7 +105,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // Optional. When not specified, the default will act as if the Filter were specified as + // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + // This searches nested groups by default. // +optional Filter string `json:"filter,omitempty"` diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 0f94c50a1..486590530 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -88,18 +88,15 @@ spec: and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying - lower-case "dn". Optional. When not specified, the default - will act as if the GroupName were specified as "dn" (distinguished - name). + lower-case "dn". Optional. When not specified, this defaults + to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". - When not specified, no group search will be performed and authenticated - users will not belong to any groups from the ActiveDirectory - provider. Also, when not specified, the values of Filter and - Attributes are ignored. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -111,7 +108,8 @@ spec: about ActiveDirectory 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 "member={}". + the default will act as if the Filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + This searches nested groups by default. type: string type: object host: @@ -146,33 +144,28 @@ spec: type: string username: description: Username specifies the name of the attribute - in the ActiveDirectory entry whose value shall become the - username of the user after a successful authentication. - This would typically be the same attribute name used in - Optional, when empty this defaults to "sAMAccountName". + in Active Directory entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in Optional, when + empty this defaults to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will search the whole directory - tree. Note that if your bind user only has permission to search - a subtree, this must be specified. Search a subtree will also - be faster. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: - description: Filter is the ActiveDirectory search filter which - should be applied when searching for users. The pattern "{}" - must occur in the filter at least once 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 ActiveDirectory 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. + description: Filter is the search filter which should be applied + when searching for users. The pattern "{}" must occur in the + filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 288c802d9..9cce14769 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -798,8 +798,8 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, the values of Filter and Attributes are ignored. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to "sAMAccountName". |=== @@ -875,8 +875,8 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will search the whole directory tree. Note that if your bind user only has permission to search a subtree, this must be specified. Search a subtree will also be faster. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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. +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index dc108466c..791e47a6d 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -46,7 +46,7 @@ type ActiveDirectoryIdentityProviderBind struct { } type ActiveDirectoryIdentityProviderUserSearchAttributes struct { - // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // Optional, when empty this defaults to "sAMAccountName". // +optional @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // Optional. When not specified, this defaults to "sAMAccountName". // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,20 +72,17 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will search the whole directory tree. - // Note that if your bind user only has permission to search a subtree, this must be specified. - // Search a subtree will also be faster. + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` - // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter at least once 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 ActiveDirectory filters, see + // 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. + // Optional. When not specified, the default will be + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -97,9 +94,8 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. - // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and - // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, - // the values of Filter and Attributes are ignored. + // "ou=groups,dc=example,dc=com". + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` @@ -109,7 +105,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // Optional. When not specified, the default will act as if the Filter were specified as + // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + // This searches nested groups by default. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 0f94c50a1..486590530 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -88,18 +88,15 @@ spec: and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying - lower-case "dn". Optional. When not specified, the default - will act as if the GroupName were specified as "dn" (distinguished - name). + lower-case "dn". Optional. When not specified, this defaults + to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". - When not specified, no group search will be performed and authenticated - users will not belong to any groups from the ActiveDirectory - provider. Also, when not specified, the values of Filter and - Attributes are ignored. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -111,7 +108,8 @@ spec: about ActiveDirectory 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 "member={}". + the default will act as if the Filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + This searches nested groups by default. type: string type: object host: @@ -146,33 +144,28 @@ spec: type: string username: description: Username specifies the name of the attribute - in the ActiveDirectory entry whose value shall become the - username of the user after a successful authentication. - This would typically be the same attribute name used in - Optional, when empty this defaults to "sAMAccountName". + in Active Directory entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in Optional, when + empty this defaults to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will search the whole directory - tree. Note that if your bind user only has permission to search - a subtree, this must be specified. Search a subtree will also - be faster. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: - description: Filter is the ActiveDirectory search filter which - should be applied when searching for users. The pattern "{}" - must occur in the filter at least once 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 ActiveDirectory 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. + description: Filter is the search filter which should be applied + when searching for users. The pattern "{}" must occur in the + filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 8d99ecc48..ed0623c6b 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -798,8 +798,8 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, the values of Filter and Attributes are ignored. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to "sAMAccountName". |=== @@ -875,8 +875,8 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will search the whole directory tree. Note that if your bind user only has permission to search a subtree, this must be specified. Search a subtree will also be faster. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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. +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index dc108466c..791e47a6d 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -46,7 +46,7 @@ type ActiveDirectoryIdentityProviderBind struct { } type ActiveDirectoryIdentityProviderUserSearchAttributes struct { - // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // Optional, when empty this defaults to "sAMAccountName". // +optional @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // Optional. When not specified, this defaults to "sAMAccountName". // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,20 +72,17 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will search the whole directory tree. - // Note that if your bind user only has permission to search a subtree, this must be specified. - // Search a subtree will also be faster. + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` - // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter at least once 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 ActiveDirectory filters, see + // 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. + // Optional. When not specified, the default will be + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -97,9 +94,8 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. - // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and - // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, - // the values of Filter and Attributes are ignored. + // "ou=groups,dc=example,dc=com". + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` @@ -109,7 +105,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // Optional. When not specified, the default will act as if the Filter were specified as + // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + // This searches nested groups by default. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 0f94c50a1..486590530 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -88,18 +88,15 @@ spec: and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying - lower-case "dn". Optional. When not specified, the default - will act as if the GroupName were specified as "dn" (distinguished - name). + lower-case "dn". Optional. When not specified, this defaults + to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". - When not specified, no group search will be performed and authenticated - users will not belong to any groups from the ActiveDirectory - provider. Also, when not specified, the values of Filter and - Attributes are ignored. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -111,7 +108,8 @@ spec: about ActiveDirectory 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 "member={}". + the default will act as if the Filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + This searches nested groups by default. type: string type: object host: @@ -146,33 +144,28 @@ spec: type: string username: description: Username specifies the name of the attribute - in the ActiveDirectory entry whose value shall become the - username of the user after a successful authentication. - This would typically be the same attribute name used in - Optional, when empty this defaults to "sAMAccountName". + in Active Directory entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in Optional, when + empty this defaults to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will search the whole directory - tree. Note that if your bind user only has permission to search - a subtree, this must be specified. Search a subtree will also - be faster. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: - description: Filter is the ActiveDirectory search filter which - should be applied when searching for users. The pattern "{}" - must occur in the filter at least once 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 ActiveDirectory 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. + description: Filter is the search filter which should be applied + when searching for users. The pattern "{}" must occur in the + filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index e999bf693..45191ade8 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -798,8 +798,8 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, the values of Filter and Attributes are ignored. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to "sAMAccountName". |=== @@ -875,8 +875,8 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will search the whole directory tree. Note that if your bind user only has permission to search a subtree, this must be specified. Search a subtree will also be faster. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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. +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index dc108466c..791e47a6d 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -46,7 +46,7 @@ type ActiveDirectoryIdentityProviderBind struct { } type ActiveDirectoryIdentityProviderUserSearchAttributes struct { - // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // Optional, when empty this defaults to "sAMAccountName". // +optional @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // Optional. When not specified, this defaults to "sAMAccountName". // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,20 +72,17 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will search the whole directory tree. - // Note that if your bind user only has permission to search a subtree, this must be specified. - // Search a subtree will also be faster. + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` - // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter at least once 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 ActiveDirectory filters, see + // 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. + // Optional. When not specified, the default will be + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -97,9 +94,8 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. - // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and - // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, - // the values of Filter and Attributes are ignored. + // "ou=groups,dc=example,dc=com". + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` @@ -109,7 +105,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // Optional. When not specified, the default will act as if the Filter were specified as + // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + // This searches nested groups by default. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 0f94c50a1..486590530 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -88,18 +88,15 @@ spec: and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying - lower-case "dn". Optional. When not specified, the default - will act as if the GroupName were specified as "dn" (distinguished - name). + lower-case "dn". Optional. When not specified, this defaults + to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". - When not specified, no group search will be performed and authenticated - users will not belong to any groups from the ActiveDirectory - provider. Also, when not specified, the values of Filter and - Attributes are ignored. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -111,7 +108,8 @@ spec: about ActiveDirectory 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 "member={}". + the default will act as if the Filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + This searches nested groups by default. type: string type: object host: @@ -146,33 +144,28 @@ spec: type: string username: description: Username specifies the name of the attribute - in the ActiveDirectory entry whose value shall become the - username of the user after a successful authentication. - This would typically be the same attribute name used in - Optional, when empty this defaults to "sAMAccountName". + in Active Directory entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in Optional, when + empty this defaults to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will search the whole directory - tree. Note that if your bind user only has permission to search - a subtree, this must be specified. Search a subtree will also - be faster. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: - description: Filter is the ActiveDirectory search filter which - should be applied when searching for users. The pattern "{}" - must occur in the filter at least once 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 ActiveDirectory 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. + description: Filter is the search filter which should be applied + when searching for users. The pattern "{}" must occur in the + filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 2eccb623a..146d458be 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -798,8 +798,8 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, the values of Filter and Attributes are ignored. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to "sAMAccountName". |=== @@ -875,8 +875,8 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will search the whole directory tree. Note that if your bind user only has permission to search a subtree, this must be specified. Search a subtree will also be faster. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 ActiveDirectory 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. +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index dc108466c..791e47a6d 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -46,7 +46,7 @@ type ActiveDirectoryIdentityProviderBind struct { } type ActiveDirectoryIdentityProviderUserSearchAttributes struct { - // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // Optional, when empty this defaults to "sAMAccountName". // +optional @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // Optional. When not specified, this defaults to "sAMAccountName". // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,20 +72,17 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will search the whole directory tree. - // Note that if your bind user only has permission to search a subtree, this must be specified. - // Search a subtree will also be faster. + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` - // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter at least once 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 ActiveDirectory filters, see + // 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. + // Optional. When not specified, the default will be + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -97,9 +94,8 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. - // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and - // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, - // the values of Filter and Attributes are ignored. + // "ou=groups,dc=example,dc=com". + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` @@ -109,7 +105,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // Optional. When not specified, the default will act as if the Filter were specified as + // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + // This searches nested groups by default. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 0f94c50a1..486590530 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -88,18 +88,15 @@ spec: and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying - lower-case "dn". Optional. When not specified, the default - will act as if the GroupName were specified as "dn" (distinguished - name). + lower-case "dn". Optional. When not specified, this defaults + to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". - When not specified, no group search will be performed and authenticated - users will not belong to any groups from the ActiveDirectory - provider. Also, when not specified, the values of Filter and - Attributes are ignored. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -111,7 +108,8 @@ spec: about ActiveDirectory 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 "member={}". + the default will act as if the Filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + This searches nested groups by default. type: string type: object host: @@ -146,33 +144,28 @@ spec: type: string username: description: Username specifies the name of the attribute - in the ActiveDirectory entry whose value shall become the - username of the user after a successful authentication. - This would typically be the same attribute name used in - Optional, when empty this defaults to "sAMAccountName". + in Active Directory entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in Optional, when + empty this defaults to "sAMAccountName". type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". - Optional, when not specified it will search the whole directory - tree. Note that if your bind user only has permission to search - a subtree, this must be specified. Search a subtree will also - be faster. + Optional, when not specified it will be based on the result + of a query for the default naming context. type: string filter: - description: Filter is the ActiveDirectory search filter which - should be applied when searching for users. The pattern "{}" - must occur in the filter at least once 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 ActiveDirectory 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. + description: Filter is the search filter which should be applied + when searching for users. The pattern "{}" must occur in the + filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index dc108466c..791e47a6d 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -46,7 +46,7 @@ type ActiveDirectoryIdentityProviderBind struct { } type ActiveDirectoryIdentityProviderUserSearchAttributes struct { - // Username specifies the name of the attribute in the ActiveDirectory entry whose value shall become the username + // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in // Optional, when empty this defaults to "sAMAccountName". // +optional @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). + // Optional. When not specified, this defaults to "sAMAccountName". // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,20 +72,17 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will search the whole directory tree. - // Note that if your bind user only has permission to search a subtree, this must be specified. - // Search a subtree will also be faster. + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` - // Filter is the ActiveDirectory search filter which should be applied when searching for users. The pattern "{}" must occur + // Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter at least once 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 ActiveDirectory filters, see + // 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. + // Optional. When not specified, the default will be + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -97,9 +94,8 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. - // "ou=groups,dc=example,dc=com". When not specified, no group search will be performed and - // authenticated users will not belong to any groups from the ActiveDirectory provider. Also, when not specified, - // the values of Filter and Attributes are ignored. + // "ou=groups,dc=example,dc=com". + // Optional, when not specified it will be based on the result of a query for the default naming context. // +optional Base string `json:"base,omitempty"` @@ -109,7 +105,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "member={}". + // Optional. When not specified, the default will act as if the Filter were specified as + // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + // This searches nested groups by default. // +optional Filter string `json:"filter,omitempty"` diff --git a/go.mod b/go.mod index 7c91fc2ea..584c25a58 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/go-logr/stdr v0.4.0 github.com/go-openapi/spec v0.20.3 // indirect github.com/gofrs/flock v0.8.1 - github.com/gofrs/uuid v3.2.0+incompatible github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.6 github.com/google/gofuzz v1.2.0 diff --git a/go.sum b/go.sum index fce7b4427..6c545de39 100644 --- a/go.sum +++ b/go.sum @@ -554,7 +554,6 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= From 1050f39789caf3e4a0c6434e5434e83cb4adf38e Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 22 Jul 2021 16:15:44 -0700 Subject: [PATCH 21/36] Integration test deactivated ad account --- test/integration/supervisor_login_test.go | 123 +++++++++++++++++----- test/testlib/env.go | 62 ++++++----- 2 files changed, 127 insertions(+), 58 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 2da945b26..d820879f6 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -47,6 +47,8 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenSubjectToMatch string wantDownstreamIDTokenUsernameToMatch string wantDownstreamIDTokenGroups []string + wantErrorDescription string + wantErrorType string }{ { name: "oidc with default username and groups claim settings", @@ -153,6 +155,7 @@ func TestSupervisorLogin(t *testing.T) { env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login httpClient, + false, ) }, // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute @@ -218,6 +221,7 @@ func TestSupervisorLogin(t *testing.T) { env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login httpClient, + false, ) }, // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute @@ -271,6 +275,7 @@ func TestSupervisorLogin(t *testing.T) { env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue, // username to present to server during login env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login httpClient, + false, ) }, // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute @@ -283,6 +288,53 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountNames, }, + { + name: "logging in to activedirectory with a deactivated user fails", + maybeSkip: func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + if env.SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } + }, + createIDP: func(t *testing.T) { + t.Helper() + secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, + }, + ) + adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ + Host: env.SupervisorUpstreamActiveDirectory.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), + }, + Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ + SecretName: secret.Name, + }, + }, idpv1alpha1.ActiveDirectoryPhaseReady) + expectedMsg := fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) + }, + requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { + requestAuthorizationUsingLDAPIdentityProvider(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamActiveDirectory.TestDeactivatedUserSAMAccountNameValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestDeactivatedUserPassword, // password to present to server during login + httpClient, + true, + ) + }, + wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", + wantErrorType: "access_denied", + }, } for _, test := range tests { tt := test @@ -295,6 +347,7 @@ func TestSupervisorLogin(t *testing.T) { tt.wantDownstreamIDTokenSubjectToMatch, tt.wantDownstreamIDTokenUsernameToMatch, tt.wantDownstreamIDTokenGroups, + tt.wantErrorDescription, tt.wantErrorType, ) }) } @@ -355,6 +408,7 @@ func testSupervisorLogin( createIDP func(t *testing.T), requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client), wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string, + wantErrorDescription string, wantErrorType string, ) { env := testlib.IntegrationEnv(t) @@ -482,40 +536,47 @@ func testSupervisorLogin( // Expect that our callback handler was invoked. callback := localCallbackServer.waitForCallback(10 * time.Second) t.Logf("got callback request: %s", testlib.MaskTokens(callback.URL.String())) - require.Equal(t, stateParam.String(), callback.URL.Query().Get("state")) - require.ElementsMatch(t, []string{"openid", "pinniped:request-audience", "offline_access"}, strings.Split(callback.URL.Query().Get("scope"), " ")) - authcode := callback.URL.Query().Get("code") - require.NotEmpty(t, authcode) + if wantErrorType == "" { + require.Equal(t, stateParam.String(), callback.URL.Query().Get("state")) + require.ElementsMatch(t, []string{"openid", "pinniped:request-audience", "offline_access"}, strings.Split(callback.URL.Query().Get("scope"), " ")) + authcode := callback.URL.Query().Get("code") + require.NotEmpty(t, authcode) - // Call the token endpoint to get tokens. - tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier()) - require.NoError(t, err) + // Call the token endpoint to get tokens. + tokenResponse, err := downstreamOAuth2Config.Exchange(oidcHTTPClientContext, authcode, pkceParam.Verifier()) + require.NoError(t, err) - expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"} - verifyTokenResponse(t, - tokenResponse, discovery, downstreamOAuth2Config, nonceParam, - expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups) + expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"} + verifyTokenResponse(t, + tokenResponse, discovery, downstreamOAuth2Config, nonceParam, + expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups) - // token exchange on the original token - doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery) + // token exchange on the original token + doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery) - // Use the refresh token to get new tokens - refreshSource := downstreamOAuth2Config.TokenSource(oidcHTTPClientContext, &oauth2.Token{RefreshToken: tokenResponse.RefreshToken}) - refreshedTokenResponse, err := refreshSource.Token() - require.NoError(t, err) + // Use the refresh token to get new tokens + refreshSource := downstreamOAuth2Config.TokenSource(oidcHTTPClientContext, &oauth2.Token{RefreshToken: tokenResponse.RefreshToken}) + refreshedTokenResponse, err := refreshSource.Token() + require.NoError(t, err) - // When refreshing, expect to get an "at_hash" claim, but no "nonce" claim. - expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"} - verifyTokenResponse(t, - refreshedTokenResponse, discovery, downstreamOAuth2Config, "", - expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups) + // When refreshing, expect to get an "at_hash" claim, but no "nonce" claim. + expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"} + verifyTokenResponse(t, + refreshedTokenResponse, discovery, downstreamOAuth2Config, "", + expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups) - require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken) - require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken) - require.NotEqual(t, tokenResponse.Extra("id_token"), refreshedTokenResponse.Extra("id_token")) + require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken) + require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken) + require.NotEqual(t, tokenResponse.Extra("id_token"), refreshedTokenResponse.Extra("id_token")) - // token exchange on the refreshed token - doTokenExchange(t, &downstreamOAuth2Config, refreshedTokenResponse, httpClient, discovery) + // token exchange on the refreshed token + doTokenExchange(t, &downstreamOAuth2Config, refreshedTokenResponse, httpClient, discovery) + } else { + errorDescription := callback.URL.Query().Get("error_description") + errorType := callback.URL.Query().Get("error") + require.Equal(t, errorDescription, wantErrorDescription) + require.Equal(t, errorType, wantErrorType) + } } func verifyTokenResponse( @@ -602,7 +663,7 @@ func requestAuthorizationUsingOIDCIdentityProvider(t *testing.T, downstreamAutho browsertest.WaitForURL(t, page, callbackURLPattern) } -func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAuthorizeURL, upstreamUsername, upstreamPassword string, httpClient *http.Client) { +func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAuthorizeURL, upstreamUsername, upstreamPassword string, httpClient *http.Client, wantErr bool) { t.Helper() ctx, cancelFunc := context.WithTimeout(context.Background(), time.Minute) @@ -645,7 +706,11 @@ func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAutho redirectLocation := authResponse.Header.Get("Location") require.Contains(t, redirectLocation, "127.0.0.1") require.Contains(t, redirectLocation, "/callback") - require.Contains(t, redirectLocation, "code=") + if wantErr { + require.Contains(t, redirectLocation, "error_description") + } else { + require.Contains(t, redirectLocation, "code=") + } // Follow the redirect. callbackRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, redirectLocation, nil) diff --git a/test/testlib/env.go b/test/testlib/env.go index ee83e1c78..875964ef8 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -82,24 +82,26 @@ type TestOIDCUpstream struct { } type TestLDAPUpstream struct { - Host string `json:"host"` - StartTLSOnlyHost string `json:"startTLSOnlyHost"` - CABundle string `json:"caBundle"` - BindUsername string `json:"bindUsername"` - BindPassword string `json:"bindPassword"` - UserSearchBase string `json:"userSearchBase"` - GroupSearchBase string `json:"groupSearchBase"` - 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"` - TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` - TestUserDirectGroupsDNs []string `json:"testUserDirectGroupsDNs"` //nolint:golint // this is "distinguished names", not "DNS" - TestUserSAMAccountNameValue string `json:"testUserSAMAccountNameValue"` - TestUserIndirectGroupsSAMAccountNames []string `json:"TestUserIndirectGroupsSAMAccountNames"` + Host string `json:"host"` + StartTLSOnlyHost string `json:"startTLSOnlyHost"` + CABundle string `json:"caBundle"` + BindUsername string `json:"bindUsername"` + BindPassword string `json:"bindPassword"` + UserSearchBase string `json:"userSearchBase"` + GroupSearchBase string `json:"groupSearchBase"` + 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"` + TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` + TestUserDirectGroupsDNs []string `json:"testUserDirectGroupsDNs"` //nolint:golint // this is "distinguished names", not "DNS" + TestUserSAMAccountNameValue string `json:"testUserSAMAccountNameValue"` + TestUserIndirectGroupsSAMAccountNames []string `json:"TestUserIndirectGroupsSAMAccountNames"` + TestDeactivatedUserSAMAccountNameValue string `json:"TestDeactivatedUserSAMAccountNameValue"` + TestDeactivatedUserPassword string `json:"TestDeactivatedUserPassword"` } // ProxyEnv returns a set of environment variable strings (e.g., to combine with os.Environ()) which set up the configured test HTTP proxy. @@ -271,17 +273,19 @@ func loadEnvVars(t *testing.T, result *TestEnv) { } result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{ - Host: wantEnv("PINNIPED_TEST_AD_HOST", ""), - CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")), - BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""), - BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""), - TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), - TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), - TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), - TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), - TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), - TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), - TestUserIndirectGroupsSAMAccountNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME", ""), ";")), + Host: wantEnv("PINNIPED_TEST_AD_HOST", ""), + CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")), + BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""), + BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""), + TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), + TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), + TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), + TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), + TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), + TestUserIndirectGroupsSAMAccountNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME", ""), ";")), + TestDeactivatedUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_SAMACCOUNTNAME", ""), + TestDeactivatedUserPassword: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_PASSWORD", ""), } sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) From 5d23068690571bf08eff400232d328f1842a7330 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Fri, 23 Jul 2021 08:47:56 -0700 Subject: [PATCH 22/36] Removed a todo that was resolved --- .../active_directory_upstream_watcher_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 6d11d0cb3..6fbd2d733 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -296,11 +296,6 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { activeDirectoryConnectionValidTrueCondition(gen, secretVersion), searchBaseFoundInConfigCondition(gen), tlsConfigurationValidLoadedTrueCondition(gen), - // TODO should there be a condition when you just get it from the config? is that worth reporting? - // I'm thinking maybe no since it's not a network call or anything... it's just like any other field in the - // spec that we don't bother to report on. - // Although perhaps it would be weirder to have a condition that only sometimes exists? And it's a useful - // way to communicate internally. } } From cc3875f048ea1e3ab4cd1d0b3e93113192703fec Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Mon, 26 Jul 2021 16:03:12 -0700 Subject: [PATCH 23/36] PR feedback --- hack/prepare-for-integration-tests.sh | 11 ++- .../active_directory_upstream_watcher.go | 6 +- .../active_directory_upstream_watcher_test.go | 9 +- .../upstreamwatchers/upstream_watchers.go | 18 ++-- internal/oidc/auth/auth_handler_test.go | 99 +++++++++++++++++++ .../idp_discovery_handler_test.go | 4 + internal/upstreamldap/upstreamldap.go | 4 +- test/integration/supervisor_login_test.go | 78 ++++++++++++++- test/testlib/env.go | 6 ++ 9 files changed, 209 insertions(+), 26 deletions(-) diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index 30399f003..25581795c 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -84,7 +84,7 @@ while (("$#")); do shift # If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error if [[ "$#" == "0" || "$1" == -* ]]; then - log_error "-g|--get-active-directory-vars requires a script name to be specified" + log_error "--get-active-directory-vars requires a script name to be specified" exit 1 fi get_active_directory_vars=$1 @@ -107,10 +107,11 @@ if [[ "$help" == "yes" ]]; then log_note " $me [flags]" log_note log_note "Flags:" - log_note " -h, --help: print this usage" - log_note " -c, --clean: destroy the current kind cluster and make a new one" - log_note " -g, --api-group-suffix: deploy Pinniped with an alternate API group suffix" - log_note " -s, --skip-build: reuse the most recently built image of the app instead of building" + log_note " -h, --help: print this usage" + log_note " -c, --clean: destroy the current kind cluster and make a new one" + log_note " -g, --api-group-suffix: deploy Pinniped with an alternate API group suffix" + log_note " -s, --skip-build: reuse the most recently built image of the app instead of building" + log_note " --get-active-directory-vars: specify a script that exports active directory environment variables" exit 1 fi diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index c6dcd0e77..60702e5a2 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -100,9 +100,9 @@ func (s *activeDirectoryUpstreamGenericLDAPSpec) DetectAndSetSearchBase(ctx cont if config.GroupSearch.Base != "" && config.UserSearch.Base != "" { // Both were already set in spec so just return; no need to query the RootDSE return &v1alpha1.Condition{ - Type: "SearchBaseFound", + Type: upstreamwatchers.TypeSearchBaseFound, Status: v1alpha1.ConditionTrue, - Reason: "Success", + Reason: upstreamwatchers.ReasonUsingConfigurationFromSpec, Message: "Using search base from ActiveDirectoryIdentityProvider config.", } } @@ -115,7 +115,7 @@ func (s *activeDirectoryUpstreamGenericLDAPSpec) DetectAndSetSearchBase(ctx cont return &v1alpha1.Condition{ Type: upstreamwatchers.TypeSearchBaseFound, Status: v1alpha1.ConditionFalse, - Reason: "Error", + Reason: upstreamwatchers.ReasonErrorFetchingSearchBase, Message: fmt.Sprintf(`Error finding search base: %s`, err.Error()), } } diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 6fbd2d733..932c4816d 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -273,7 +273,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Type: "SearchBaseFound", Status: "True", LastTransitionTime: now, - Reason: "Success", + Reason: "UsingConfigurationFromSpec", Message: "Using search base from ActiveDirectoryIdentityProvider config.", ObservedGeneration: gen, } @@ -284,7 +284,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Type: "SearchBaseFound", Status: "False", LastTransitionTime: now, - Reason: "Error", + Reason: "ErrorFetchingSearchBase", Message: message, ObservedGeneration: gen, } @@ -878,7 +878,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { testHost, testBindUsername, testBindUsername), ObservedGeneration: 1234, }, - searchBaseFoundErrorCondition(1234, "Error finding search base: error binding as \"test-bind-username\" before user search: some bind error"), + searchBaseFoundErrorCondition(1234, "Error finding search base: error binding as \"test-bind-username\" before querying for defaultNamingContext: some bind error"), tlsConfigurationValidLoadedTrueCondition(1234), }, }, @@ -1344,9 +1344,6 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, { name: "when the input activedirectoryidentityprovider leaves group search base blank and query for defaultNamingContext fails", - // TODO is this a fatal error? I think so because leaving the search base blank and trying anyway does not seem expected. - // queries with an empty search base could potentially succeed but return something unexpected, like if you were - // pointing at global catalog but not intending to use the GC functionality... inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go index 58522d60a..cbfb51299 100644 --- a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -33,13 +33,15 @@ const ( TestLDAPConnectionTimeout = 90 * time.Second // Constants related to conditions. - typeBindSecretValid = "BindSecretValid" - typeTLSConfigurationValid = "TLSConfigurationValid" - typeLDAPConnectionValid = "LDAPConnectionValid" - TypeSearchBaseFound = "SearchBaseFound" - reasonLDAPConnectionError = "LDAPConnectionError" - noTLSConfigurationMessage = "no TLS configuration provided" - loadedTLSConfigurationMessage = "loaded TLS configuration" + typeBindSecretValid = "BindSecretValid" + typeTLSConfigurationValid = "TLSConfigurationValid" + typeLDAPConnectionValid = "LDAPConnectionValid" + TypeSearchBaseFound = "SearchBaseFound" + reasonLDAPConnectionError = "LDAPConnectionError" + noTLSConfigurationMessage = "no TLS configuration provided" + loadedTLSConfigurationMessage = "loaded TLS configuration" + ReasonUsingConfigurationFromSpec = "UsingConfigurationFromSpec" + ReasonErrorFetchingSearchBase = "ErrorFetchingSearchBase" ) // An in-memory cache with an entry for each ActiveDirectoryIdentityProvider, to keep track of which ResourceVersion @@ -187,7 +189,7 @@ func HasPreviousSuccessfulSearchBaseConditionForCurrentGeneration(secretVersionC // 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, if any. validatedSettings := secretVersionCache.ValidatedSettingsByName[upstreamName] - // Reload the TLS vs StartTLS setting that was previously validated. + // Reload the user search and group search base settings that were previously validated. config.UserSearch.Base = validatedSettings.UserSearchBase config.GroupSearch.Base = validatedSettings.GroupSearchBase return true diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index e12c10044..1d6f0fe3a 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -612,6 +612,17 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "Bad Gateway: unexpected error during upstream authentication\n", }, + { + name: "error during upstream Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.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(), @@ -624,6 +635,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "wrong upstream password for Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.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(), @@ -636,6 +659,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "wrong upstream username for Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr("wrong-username"), + customPasswordHeader: pointer.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(), @@ -648,6 +683,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "missing upstream username on request for Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: nil, // do not send header + customPasswordHeader: pointer.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(), @@ -660,6 +707,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "missing upstream password on request for Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.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(), @@ -764,6 +823,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantBodyString: "", }, + { + name: "downstream scopes do not match what is configured for client using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.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(), @@ -789,6 +860,16 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, + { + name: "missing response type in request using Active Directory upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&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(), @@ -1122,6 +1203,15 @@ 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 Active Directory", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&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 @@ -1131,6 +1221,15 @@ 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: OIDC, LDAP and AD", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).WithLDAP(&upstreamLDAPIdentityProvider).WithActiveDirectory(&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(), diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go index f48368189..f6bb641b5 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go @@ -41,6 +41,7 @@ func TestIDPDiscovery(t *testing.T) { {Name: "a-some-oidc-idp", Type: "oidc"}, {Name: "x-some-idp", Type: "ldap"}, {Name: "x-some-idp", Type: "oidc"}, + {Name: "y-some-ad-idp", Type: "activedirectory"}, {Name: "z-some-ad-idp", Type: "activedirectory"}, {Name: "z-some-ldap-idp", Type: "ldap"}, {Name: "z-some-oidc-idp", Type: "oidc"}, @@ -49,6 +50,7 @@ func TestIDPDiscovery(t *testing.T) { wantSecondResponseBodyJSON: &response{ IDPs: []identityProviderResponse{ {Name: "some-other-ad-idp-1", Type: "activedirectory"}, + {Name: "some-other-ad-idp-2", Type: "activedirectory"}, {Name: "some-other-ldap-idp-1", Type: "ldap"}, {Name: "some-other-ldap-idp-2", Type: "ldap"}, {Name: "some-other-oidc-idp-1", Type: "oidc"}, @@ -76,6 +78,7 @@ func TestIDPDiscovery(t *testing.T) { WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ldap-idp"}). WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "x-some-idp"}). WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ad-idp"}). + WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "y-some-ad-idp"}). Build() handler := NewHandler(idpLister) @@ -108,6 +111,7 @@ func TestIDPDiscovery(t *testing.T) { }) idpLister.SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-2"}, &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-1"}, }) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 443e23b89..099ad3a3c 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -393,7 +393,7 @@ func (p *Provider) validateConfig() error { } func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, error) { - t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()}) + t := trace.FromContext(ctx).Nest("slow ldap attempt when searching for default naming context", trace.Field{Key: "providerName", Value: p.GetName()}) defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches conn, err := p.dial(ctx) @@ -406,7 +406,7 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e err = conn.Bind(p.c.BindUsername, p.c.BindPassword) if err != nil { p.traceAuthFailure(t, err) - return "", fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err) + return "", fmt.Errorf(`error binding as "%s" before querying for defaultNamingContext: %w`, p.c.BindUsername, err) } searchResult, err := conn.Search(p.defaultNamingContextRequest()) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index d820879f6..bf4dd9e1f 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -281,12 +281,79 @@ func TestSupervisorLogin(t *testing.T) { // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( "ldaps://" + env.SupervisorUpstreamActiveDirectory.Host + - "?base=" + url.QueryEscape("DC=activedirectory,DC=test,DC=pinniped,DC=dev") + + "?base=" + url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase) + "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountNames, + }, { + name: "activedirectory with custom options", + maybeSkip: func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + if env.SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } + }, + createIDP: func(t *testing.T) { + t.Helper() + secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, + }, + ) + adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ + Host: env.SupervisorUpstreamActiveDirectory.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), + }, + Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ + SecretName: secret.Name, + }, + UserSearch: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearch{ + Base: env.SupervisorUpstreamActiveDirectory.UserSearchBase, + Filter: env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeName + "={}", + Attributes: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{ + Username: env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeName, + }, + }, + GroupSearch: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearch{ + Filter: "member={}", // excluding nested groups + Base: env.SupervisorUpstreamActiveDirectory.GroupSearchBase, + Attributes: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{ + GroupName: "dn", + }, + }, + }, idpv1alpha1.ActiveDirectoryPhaseReady) + expectedMsg := fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) + }, + requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { + requestAuthorizationUsingLDAPIdentityProvider(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login + httpClient, + false, + ) + }, + // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute + wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( + "ldaps://" + env.SupervisorUpstreamActiveDirectory.Host + + "?base=" + url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase) + + "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, + ), + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue), + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs, }, { name: "logging in to activedirectory with a deactivated user fails", @@ -395,11 +462,18 @@ func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, ad } } + expectedUserSearchReason := "" + if adIDP.Spec.UserSearch.Base == "" || adIDP.Spec.GroupSearch.Base == "" { + expectedUserSearchReason = "Success" + } else { + expectedUserSearchReason = "UsingConfigurationFromSpec" + } + require.ElementsMatch(t, [][]string{ {"BindSecretValid", "True", "Success"}, {"TLSConfigurationValid", "True", "Success"}, {"LDAPConnectionValid", "True", "Success"}, - {"SearchBaseFound", "True", "Success"}, + {"SearchBaseFound", "True", expectedUserSearchReason}, }, conditionsSummary) } diff --git a/test/testlib/env.go b/test/testlib/env.go index 875964ef8..148086e27 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -88,6 +88,7 @@ type TestLDAPUpstream struct { BindUsername string `json:"bindUsername"` BindPassword string `json:"bindPassword"` UserSearchBase string `json:"userSearchBase"` + DefaultNamingContextSearchBase string `json:"defaultNamingContextSearchBase"` GroupSearchBase string `json:"groupSearchBase"` TestUserDN string `json:"testUserDN"` TestUserCN string `json:"testUserCN"` @@ -281,11 +282,16 @@ func loadEnvVars(t *testing.T, result *TestEnv) { TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserMailAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_VALUE", ""), + TestUserMailAttributeName: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_NAME", ""), TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), TestUserIndirectGroupsSAMAccountNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME", ""), ";")), TestDeactivatedUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_SAMACCOUNTNAME", ""), TestDeactivatedUserPassword: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_PASSWORD", ""), + DefaultNamingContextSearchBase: wantEnv("PINNIPED_TEST_AD_DEFAULTNAMINGCONTEXT_DN", ""), + UserSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""), + GroupSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""), } sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) From 53b58f65b2a297f84f602da92be089c4d1d1f8de Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Mon, 26 Jul 2021 16:32:46 -0700 Subject: [PATCH 24/36] Add integration test for wrong password with ldap --- internal/upstreamldap/upstreamldap.go | 9 +++- test/integration/supervisor_login_test.go | 59 +++++++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 099ad3a3c..06d19ac82 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -398,14 +398,14 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e conn, err := p.dial(ctx) if err != nil { - p.traceAuthFailure(t, err) + p.traceSearchBaseDiscoveryFailure(t, err) 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 { - p.traceAuthFailure(t, err) + p.traceSearchBaseDiscoveryFailure(t, err) return "", fmt.Errorf(`error binding as "%s" before querying for defaultNamingContext: %w`, p.c.BindUsername, err) } @@ -648,3 +648,8 @@ func (p *Provider) traceAuthSuccess(t *trace.Trace) { trace.Field{Key: "authenticated", Value: true}, ) } + +func (p *Provider) traceSearchBaseDiscoveryFailure(t *trace.Trace, err error) { + t.Step("search base discovery failed", + trace.Field{Key: "reason", Value: err.Error()}) +} diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index bf4dd9e1f..ed2912a17 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -234,6 +234,65 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN), wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs, }, + { + name: "logging in to ldap with the wrong password fails", + maybeSkip: func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + }, + createIDP: func(t *testing.T) { + t.Helper() + secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword, + }, + ) + ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{ + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)), + }, + Bind: idpv1alpha1.LDAPIdentityProviderBind{ + SecretName: secret.Name, + }, + UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{ + Base: env.SupervisorUpstreamLDAP.UserSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ + Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, + }, + }, + GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ + Base: env.SupervisorUpstreamLDAP.GroupSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ + GroupName: "dn", + }, + }, + }, idpv1alpha1.LDAPPhaseReady) + expectedMsg := fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg) + }, + requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { + requestAuthorizationUsingLDAPIdentityProvider(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + "incorrect", // password to present to server during login + httpClient, + true, + ) + }, + wantErrorDescription: "The resource owner or authorization server denied the request. Username/password not accepted by LDAP provider.", + wantErrorType: "access_denied", + }, { name: "activedirectory with all default options", maybeSkip: func(t *testing.T) { From 287a5d225a5c823ff19f84abeaa28506d272ac41 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 27 Jul 2021 10:23:05 -0700 Subject: [PATCH 25/36] Change SearchBaseFound condition success reason to be a string constant --- .../active_directory_upstream_watcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 60702e5a2..d3041180a 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -128,7 +128,7 @@ func (s *activeDirectoryUpstreamGenericLDAPSpec) DetectAndSetSearchBase(ctx cont return &v1alpha1.Condition{ Type: upstreamwatchers.TypeSearchBaseFound, Status: v1alpha1.ConditionTrue, - Reason: "Success", + Reason: upstreamwatchers.ReasonSuccess, Message: "Successfully fetched defaultNamingContext to use as default search base from RootDSE.", } } From bbaa8202784221608c2977bdacd914d976c05052 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 27 Jul 2021 11:08:23 -0700 Subject: [PATCH 26/36] parsing objectGUID as human-readable string version --- go.mod | 1 + .../active_directory_upstream_watcher.go | 3 +- .../active_directory_upstream_watcher_test.go | 28 +++++- internal/upstreamldap/upstreamldap.go | 30 ++++++ internal/upstreamldap/upstreamldap_test.go | 91 +++++++++++++++++++ 5 files changed, 151 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 584c25a58..702d5e2e0 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.6 github.com/google/gofuzz v1.2.0 + github.com/google/uuid v1.1.2 // indirect github.com/gorilla/securecookie v1.1.1 github.com/gorilla/websocket v1.4.2 github.com/onsi/ginkgo v1.13.0 // indirect diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index d3041180a..677ea2b78 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -306,7 +306,8 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, Filter: adUpstreamImpl.Spec().GroupSearch().Filter(), GroupNameAttribute: adUpstreamImpl.Spec().GroupSearch().GroupNameAttribute(), }, - Dialer: c.ldapDialer, + Dialer: c.ldapDialer, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, } conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 932c4816d..d3657099a 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "errors" "fmt" + "reflect" "sort" "testing" "time" @@ -217,6 +218,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, } // Make a copy with targeted changes. @@ -531,6 +533,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -588,6 +591,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -645,6 +649,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), @@ -701,6 +706,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -824,6 +830,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -943,6 +950,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -992,6 +1000,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1181,6 +1190,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))", GroupNameAttribute: "sAMAccountName", }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1230,6 +1240,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1278,6 +1289,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1326,6 +1338,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1561,7 +1574,20 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { // The dialer that was passed in to the controller's constructor should always have been // passed through to the provider. copyOfExpectedValueForResultingCache.Dialer = dialer - require.Equal(t, copyOfExpectedValueForResultingCache, actualIDP.GetConfig()) + + // function equality is awkward. Do the check for equality separately from the rest of the config. + expectedUIDAttributeParsingOverrides := copyOfExpectedValueForResultingCache.UIDAttributeParsingOverrides + actualConfig := actualIDP.GetConfig() + actualUIDAttributeParsingOverrides := actualConfig.UIDAttributeParsingOverrides + copyOfExpectedValueForResultingCache.UIDAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} + actualConfig.UIDAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} + + require.Len(t, actualUIDAttributeParsingOverrides, 1) + require.Len(t, expectedUIDAttributeParsingOverrides, 1) + require.Equal(t, expectedUIDAttributeParsingOverrides[0].AttributeName, actualUIDAttributeParsingOverrides[0].AttributeName) + require.Equal(t, reflect.ValueOf(expectedUIDAttributeParsingOverrides[0].OverrideFunc).Pointer(), reflect.ValueOf(actualUIDAttributeParsingOverrides[0].OverrideFunc).Pointer()) + + require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig) } actualUpstreams, err := fakePinnipedClient.IDPV1alpha1().ActiveDirectoryIdentityProviders(testNamespace).List(ctx, metav1.ListOptions{}) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 06d19ac82..89bd3338e 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -17,6 +17,8 @@ import ( "strings" "time" + "github.com/google/uuid" + "github.com/go-ldap/ldap/v3" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" @@ -103,6 +105,15 @@ type ProviderConfig struct { // Dialer exists to enable testing. When nil, will use a default appropriate for production use. Dialer LDAPDialer + + // UIDAttributeParsingOverrides are mappings between an attribute name and a way to parse it when + // it comes out of LDAP. + UIDAttributeParsingOverrides []AttributeParsingOverride +} + +type AttributeParsingOverride struct { + AttributeName string + OverrideFunc func([]byte) (string, error) } // UserSearchConfig contains information about how to search for users in the upstream LDAP IDP. @@ -610,6 +621,12 @@ func (p *Provider) getSearchResultAttributeRawValueEncoded(attributeName string, ) } + for _, override := range p.c.UIDAttributeParsingOverrides { + if attributeName == override.AttributeName { + return override.OverrideFunc(attributeValue) + } + } + return base64.RawURLEncoding.EncodeToString(attributeValue), nil } @@ -653,3 +670,16 @@ func (p *Provider) traceSearchBaseDiscoveryFailure(t *trace.Trace, err error) { t.Step("search base discovery failed", trace.Field{Key: "reason", Value: err.Error()}) } + +func MicrosoftUUIDFromBinary(binaryUUID []byte) (string, error) { + uuidVal, err := uuid.FromBytes(binaryUUID) // start out with the RFC4122 version + if err != nil { + return "", err + } + // then swap it because AD stores the first 3 fields little-endian rather than the expected + // big-endian. + uuidVal[0], uuidVal[1], uuidVal[2], uuidVal[3] = uuidVal[3], uuidVal[2], uuidVal[1], uuidVal[0] + uuidVal[4], uuidVal[5] = uuidVal[5], uuidVal[4] + uuidVal[6], uuidVal[7] = uuidVal[7], uuidVal[6] + return uuidVal.String(), nil +} diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index c4482fdf1..12e7f4eb8 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -506,6 +506,64 @@ func TestEndUserAuthentication(t *testing.T) { }, }, }, + { + name: "override UID parsing to work with microsoft style objectGUIDs", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.UIDAttributeParsingOverrides = []AttributeParsingOverride{{ + AttributeName: "objectGUID", + OverrideFunc: MicrosoftUUIDFromBinary, + }} + p.UserSearch.UIDAttribute = "objectGUID" + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{testUserSearchUsernameAttribute, "objectGUID"} + })).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testUserSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute("objectGUID", []string{"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16"}), + }, + }, + }}, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.UID = "04030201-0605-0807-0910-111213141516" + }), + }, + { + name: "override UID parsing when the attribute name doesn't match what's returned does default parsing", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.UIDAttributeParsingOverrides = []AttributeParsingOverride{{ + AttributeName: "objectGUID", + OverrideFunc: MicrosoftUUIDFromBinary, + }} + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(nil), + }, { name: "when dial fails", username: testUpstreamUsername, @@ -1297,3 +1355,36 @@ func TestRealTLSDialing(t *testing.T) { }) } } + +func TestGetMicrosoftFormattedUUID(t *testing.T) { + tests := []struct { + name string + binaryUUID []byte + wantString string + wantErr string + }{ + { + name: "happy path", + binaryUUID: []byte("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16"), + wantString: "04030201-0605-0807-0910-111213141516", + }, + { + name: "not the right length", + binaryUUID: []byte("2\xf8\xb0\xaa\xb6V\xb1D\x8b(\xee"), + wantErr: "invalid UUID (got 11 bytes)", + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + actualUUIDString, err := MicrosoftUUIDFromBinary(tt.binaryUUID) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.wantString, actualUUIDString) + }) + } +} From 26c47d564feecdf3e0c463b3be0b0aac45b1bed5 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Tue, 17 Aug 2021 16:53:26 -0700 Subject: [PATCH 27/36] Make new combined sAMAccountName@domain attribute the group name Also change default username attribute to userPrincipalName --- .../active_directory_upstream_watcher.go | 27 +++- .../active_directory_upstream_watcher_test.go | 85 ++++++++++-- internal/upstreamldap/upstreamldap.go | 49 ++++++- internal/upstreamldap/upstreamldap_test.go | 127 +++++++++++++++++- test/integration/supervisor_login_test.go | 2 +- test/testlib/env.go | 80 +++++------ 6 files changed, 307 insertions(+), 63 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 677ea2b78..b83b89100 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -29,16 +29,23 @@ const ( activeDirectoryControllerName = "active-directory-upstream-observer" // Default values for active directory config. - defaultActiveDirectoryUsernameAttributeName = "sAMAccountName" - defaultActiveDirectoryUIDAttributeName = "objectGUID" - defaultActiveDirectoryGroupNameAttributeName = "sAMAccountName" + defaultActiveDirectoryUsernameAttributeName = "userPrincipalName" + defaultActiveDirectoryUIDAttributeName = "objectGUID" + + // This is not a real attribute in active directory. + // It represents a combined attribute name, sAMAccountName + "@" + domain. + // For example if your group sAMAccountName is "mammals" and your domain is + // "activedirectory.example.com", it would be mammals@activedirectory.example.com. + // This is because sAMAccountName is only unique within a domain, not a forest. + defaultActiveDirectoryGroupNameAttributeName = "sAMAccountName" + defaultActiveDirectoryGroupNameOverrideAttributeName = "pinniped:sAMAccountName@domain" // - is a person. // - is not a computer. // - is not shown in advanced view only (which would likely mean its a system created service account with advanced permissions). - // - either the sAMAccountName or the mail attribute matches the input username. + // - either the sAMAccountName, the userPrincipalName or the mail attribute matches the input username. // - the sAMAccountType is for a normal user account. - defaultActiveDirectoryUserSearchFilter = "(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={})(mail={}))(sAMAccountType=805306368))" + defaultActiveDirectoryUserSearchFilter = "(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={})(mail={})(userPrincipalName={}))(sAMAccountType=805306368))" // - is a group. // - has a member that matches the DN of the user we successfully logged in as. @@ -181,6 +188,10 @@ func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) GroupNameAttribute() str if len(g.groupSearch.Attributes.GroupName) == 0 { return defaultActiveDirectoryGroupNameAttributeName } + // you explicitly told us to use the override value + if g.groupSearch.Attributes.GroupName == defaultActiveDirectoryGroupNameOverrideAttributeName { + return defaultActiveDirectoryGroupNameAttributeName + } return g.groupSearch.Attributes.GroupName } @@ -307,7 +318,11 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, GroupNameAttribute: adUpstreamImpl.Spec().GroupSearch().GroupNameAttribute(), }, Dialer: c.ldapDialer, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + } + + if spec.GroupSearch.Attributes.GroupName == defaultActiveDirectoryGroupNameOverrideAttributeName || spec.GroupSearch.Attributes.GroupName == "" { + config.GroupAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{{AttributeName: defaultActiveDirectoryGroupNameAttributeName, OverrideFunc: upstreamldap.GroupSAMAccountNameWithDomainSuffix}} } conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index d3657099a..289e0d2f5 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -1181,8 +1181,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { BindPassword: testBindPassword, UserSearch: upstreamldap.UserSearchConfig{ Base: testUserSearchBase, - Filter: "(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={})(mail={}))(sAMAccountType=805306368))", - UsernameAttribute: "sAMAccountName", + Filter: "(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={})(mail={})(userPrincipalName={}))(sAMAccountType=805306368))", + UsernameAttribute: "userPrincipalName", UIDAttribute: "objectGUID", }, GroupSearch: upstreamldap.GroupSearchConfig{ @@ -1190,7 +1190,59 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))", GroupNameAttribute: "sAMAccountName", }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + GroupAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "sAMAccountName", OverrideFunc: upstreamldap.GroupSAMAccountNameWithDomainSuffix}}, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: allConditionsTrue(1234, "4242"), + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { + BindSecretResourceVersion: "4242", + LDAPConnectionProtocol: upstreamldap.TLS, + UserSearchBase: testUserSearchBase, + GroupSearchBase: testGroupSearchBase, + }}, + }, + { + name: "when the input activedirectoryidentityprovider group search attributes is the special cased pinniped:sAMAccountName@domain value", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} + upstream.Spec.UserSearch.Filter = "" + upstream.Spec.GroupSearch.Filter = "" + upstream.Spec.GroupSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{GroupName: "pinniped:sAMAccountName@domain"} + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + 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, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: testCABundle, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: "(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={})(mail={})(userPrincipalName={}))(sAMAccountType=805306368))", + UsernameAttribute: "userPrincipalName", + UIDAttribute: "objectGUID", + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))", + GroupNameAttribute: "sAMAccountName", + }, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + GroupAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "sAMAccountName", OverrideFunc: upstreamldap.GroupSAMAccountNameWithDomainSuffix}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1232,7 +1284,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { UserSearch: upstreamldap.UserSearchConfig{ Base: exampleDefaultNamingContext, Filter: testUserSearchFilter, - UsernameAttribute: "sAMAccountName", + UsernameAttribute: "userPrincipalName", UIDAttribute: "objectGUID", }, GroupSearch: upstreamldap.GroupSearchConfig{ @@ -1281,7 +1333,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { UserSearch: upstreamldap.UserSearchConfig{ Base: exampleDefaultNamingContext, Filter: testUserSearchFilter, - UsernameAttribute: "sAMAccountName", + UsernameAttribute: "userPrincipalName", UIDAttribute: "objectGUID", }, GroupSearch: upstreamldap.GroupSearchConfig{ @@ -1330,7 +1382,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { UserSearch: upstreamldap.UserSearchConfig{ Base: testUserSearchBase, Filter: testUserSearchFilter, - UsernameAttribute: "sAMAccountName", + UsernameAttribute: "userPrincipalName", UIDAttribute: "objectGUID", }, GroupSearch: upstreamldap.GroupSearchConfig{ @@ -1582,10 +1634,23 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { copyOfExpectedValueForResultingCache.UIDAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} actualConfig.UIDAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} - require.Len(t, actualUIDAttributeParsingOverrides, 1) - require.Len(t, expectedUIDAttributeParsingOverrides, 1) - require.Equal(t, expectedUIDAttributeParsingOverrides[0].AttributeName, actualUIDAttributeParsingOverrides[0].AttributeName) - require.Equal(t, reflect.ValueOf(expectedUIDAttributeParsingOverrides[0].OverrideFunc).Pointer(), reflect.ValueOf(actualUIDAttributeParsingOverrides[0].OverrideFunc).Pointer()) + require.Equal(t, len(expectedUIDAttributeParsingOverrides), len(actualUIDAttributeParsingOverrides)) + for i := range expectedUIDAttributeParsingOverrides { + require.Equal(t, expectedUIDAttributeParsingOverrides[i].AttributeName, actualUIDAttributeParsingOverrides[i].AttributeName) + require.Equal(t, reflect.ValueOf(expectedUIDAttributeParsingOverrides[i].OverrideFunc).Pointer(), reflect.ValueOf(actualUIDAttributeParsingOverrides[i].OverrideFunc).Pointer()) + } + + // function equality is awkward. Do the check for equality separately from the rest of the config. + expectedGroupAttributeParsingOverrides := copyOfExpectedValueForResultingCache.GroupAttributeParsingOverrides + actualGroupAttributeParsingOverrides := actualConfig.GroupAttributeParsingOverrides + copyOfExpectedValueForResultingCache.GroupAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} + actualConfig.GroupAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} + + require.Equal(t, len(expectedGroupAttributeParsingOverrides), len(actualGroupAttributeParsingOverrides)) + for i := range expectedGroupAttributeParsingOverrides { + require.Equal(t, expectedGroupAttributeParsingOverrides[i].AttributeName, actualGroupAttributeParsingOverrides[i].AttributeName) + require.Equal(t, reflect.ValueOf(expectedGroupAttributeParsingOverrides[i].OverrideFunc).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[i].OverrideFunc).Pointer()) + } require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig) } diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 89bd3338e..b6e9571a6 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -13,6 +13,7 @@ import ( "fmt" "net" "net/url" + "regexp" "sort" "strings" "time" @@ -106,14 +107,18 @@ type ProviderConfig struct { // Dialer exists to enable testing. When nil, will use a default appropriate for production use. Dialer LDAPDialer - // UIDAttributeParsingOverrides are mappings between an attribute name and a way to parse it when + // UIDAttributeParsingOverrides are mappings between an attribute name and a way to parse it as a UID when // it comes out of LDAP. UIDAttributeParsingOverrides []AttributeParsingOverride + + // GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group + // name when it comes out of LDAP. + GroupAttributeParsingOverrides []AttributeParsingOverride } type AttributeParsingOverride struct { AttributeName string - OverrideFunc func([]byte) (string, error) + OverrideFunc func(entry *ldap.Entry) (string, error) } // UserSearchConfig contains information about how to search for users in the upstream LDAP IDP. @@ -389,6 +394,15 @@ func (p *Provider) searchGroupsForUserDN(conn Conn, userDN string) ([]string, er if err != nil { return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err) } + for _, override := range p.c.GroupAttributeParsingOverrides { + if groupAttributeName == override.AttributeName { + overrideGroupName, err := override.OverrideFunc(groupEntry) + if err != nil { + return nil, fmt.Errorf("error finding groups: %w", err) + } + mappedGroupName = overrideGroupName + } + } groups = append(groups, mappedGroupName) } @@ -623,7 +637,7 @@ func (p *Provider) getSearchResultAttributeRawValueEncoded(attributeName string, for _, override := range p.c.UIDAttributeParsingOverrides { if attributeName == override.AttributeName { - return override.OverrideFunc(attributeValue) + return override.OverrideFunc(entry) } } @@ -671,7 +685,15 @@ func (p *Provider) traceSearchBaseDiscoveryFailure(t *trace.Trace, err error) { trace.Field{Key: "reason", Value: err.Error()}) } -func MicrosoftUUIDFromBinary(binaryUUID []byte) (string, error) { +func MicrosoftUUIDFromBinary(attributeName string) func(entry *ldap.Entry) (string, error) { + // validation has already been done so we can just get the attribute... + return func(entry *ldap.Entry) (string, error) { + binaryUUID := entry.GetRawAttributeValue(attributeName) + return microsoftUUIDFromBinary(binaryUUID) + } +} + +func microsoftUUIDFromBinary(binaryUUID []byte) (string, error) { uuidVal, err := uuid.FromBytes(binaryUUID) // start out with the RFC4122 version if err != nil { return "", err @@ -683,3 +705,22 @@ func MicrosoftUUIDFromBinary(binaryUUID []byte) (string, error) { uuidVal[6], uuidVal[7] = uuidVal[7], uuidVal[6] return uuidVal.String(), nil } + +func GroupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) { + sAMAccountNameAttribute := "sAMAccountName" + sAMAccountName := entry.GetAttributeValue(sAMAccountNameAttribute) + distinguishedName := entry.DN + domain, err := getDomainFromDistinguishedName(distinguishedName) + if err != nil { + return "", err + } + return sAMAccountName + "@" + domain, nil +} + +func getDomainFromDistinguishedName(distinguishedName string) (string, error) { + domainComponents := regexp.MustCompile(",DC=|,dc=").Split(distinguishedName, -1) + if len(domainComponents) == 1 { + return "", fmt.Errorf("did not find domain components in group dn: %s", distinguishedName) + } + return strings.Join(domainComponents[1:], "."), nil +} diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 12e7f4eb8..641bee40c 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -513,7 +513,7 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(func(p *ProviderConfig) { p.UIDAttributeParsingOverrides = []AttributeParsingOverride{{ AttributeName: "objectGUID", - OverrideFunc: MicrosoftUUIDFromBinary, + OverrideFunc: MicrosoftUUIDFromBinary("objectGUID"), }} p.UserSearch.UIDAttribute = "objectGUID" }), @@ -549,7 +549,7 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(func(p *ProviderConfig) { p.UIDAttributeParsingOverrides = []AttributeParsingOverride{{ AttributeName: "objectGUID", - OverrideFunc: MicrosoftUUIDFromBinary, + OverrideFunc: MicrosoftUUIDFromBinary("objectGUID"), }} }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -564,6 +564,89 @@ func TestEndUserAuthentication(t *testing.T) { }, wantAuthResponse: expectedAuthResponse(nil), }, + { + name: "override group parsing to create new group names", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "sAMAccountName" + p.GroupAttributeParsingOverrides = []AttributeParsingOverride{{ + AttributeName: "sAMAccountName", + OverrideFunc: GroupSAMAccountNameWithDomainSuffix, + }} + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{"sAMAccountName"} + }), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}), + }, + }, + { + DN: "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Animals"}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Groups = []string{"Animals@activedirectory.mycompany.example.com", "Mammals@activedirectory.mycompany.example.com"} + }), + }, + { + name: "override group parsing when domain can't be determined from dn", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "sAMAccountName" + p.GroupAttributeParsingOverrides = []AttributeParsingOverride{{ + AttributeName: "sAMAccountName", + OverrideFunc: GroupSAMAccountNameWithDomainSuffix, + }} + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{"sAMAccountName"} + }), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "no-domain-components", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}), + }, + }, + { + DN: "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Animals"}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: "error finding groups: did not find domain components in group dn: no-domain-components", + }, { name: "when dial fails", username: testUpstreamUsername, @@ -1378,7 +1461,7 @@ func TestGetMicrosoftFormattedUUID(t *testing.T) { for _, test := range tests { tt := test t.Run(tt.name, func(t *testing.T) { - actualUUIDString, err := MicrosoftUUIDFromBinary(tt.binaryUUID) + actualUUIDString, err := microsoftUUIDFromBinary(tt.binaryUUID) if tt.wantErr != "" { require.EqualError(t, err, tt.wantErr) } else { @@ -1388,3 +1471,41 @@ func TestGetMicrosoftFormattedUUID(t *testing.T) { }) } } + +func TestGetDomainFromDistinguishedName(t *testing.T) { + tests := []struct { + name string + distinguishedName string + wantDomain string + wantErr string + }{ + { + name: "happy path", + distinguishedName: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com", + wantDomain: "activedirectory.mycompany.example.com", + }, + { + name: "lowercased happy path", + distinguishedName: "cn=Mammals,ou=Users,ou=pinniped-ad,dc=activedirectory,dc=mycompany,dc=example,dc=com", + wantDomain: "activedirectory.mycompany.example.com", + }, + { + name: "no domain components", + distinguishedName: "not-a-dn", + wantErr: "did not find domain components in group dn: not-a-dn", + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + actualDomain, err := getDomainFromDistinguishedName(tt.distinguishedName) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.wantDomain, actualDomain) + }) + } +} diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index ed2912a17..1ff736218 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -345,7 +345,7 @@ func TestSupervisorLogin(t *testing.T) { ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue), - wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountNames, + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, }, { name: "activedirectory with custom options", maybeSkip: func(t *testing.T) { diff --git a/test/testlib/env.go b/test/testlib/env.go index 148086e27..7b8c71b8b 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -82,27 +82,28 @@ type TestOIDCUpstream struct { } type TestLDAPUpstream struct { - Host string `json:"host"` - StartTLSOnlyHost string `json:"startTLSOnlyHost"` - CABundle string `json:"caBundle"` - BindUsername string `json:"bindUsername"` - BindPassword string `json:"bindPassword"` - UserSearchBase string `json:"userSearchBase"` - DefaultNamingContextSearchBase string `json:"defaultNamingContextSearchBase"` - GroupSearchBase string `json:"groupSearchBase"` - 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"` - TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` - TestUserDirectGroupsDNs []string `json:"testUserDirectGroupsDNs"` //nolint:golint // this is "distinguished names", not "DNS" - TestUserSAMAccountNameValue string `json:"testUserSAMAccountNameValue"` - TestUserIndirectGroupsSAMAccountNames []string `json:"TestUserIndirectGroupsSAMAccountNames"` - TestDeactivatedUserSAMAccountNameValue string `json:"TestDeactivatedUserSAMAccountNameValue"` - TestDeactivatedUserPassword string `json:"TestDeactivatedUserPassword"` + Host string `json:"host"` + StartTLSOnlyHost string `json:"startTLSOnlyHost"` + CABundle string `json:"caBundle"` + BindUsername string `json:"bindUsername"` + BindPassword string `json:"bindPassword"` + UserSearchBase string `json:"userSearchBase"` + DefaultNamingContextSearchBase string `json:"defaultNamingContextSearchBase"` + GroupSearchBase string `json:"groupSearchBase"` + 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"` + TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` + TestUserDirectGroupsDNs []string `json:"testUserDirectGroupsDNs"` //nolint:golint // this is "distinguished names", not "DNS" + TestUserSAMAccountNameValue string `json:"testUserSAMAccountNameValue"` + TestUserIndirectGroupsSAMAccountNames []string `json:"TestUserIndirectGroupsSAMAccountNames"` + TestUserIndirectGroupsSAMAccountPlusDomainNames []string `json:"TestUserIndirectGroupsSAMAccountPlusDomainNames"` + TestDeactivatedUserSAMAccountNameValue string `json:"TestDeactivatedUserSAMAccountNameValue"` + TestDeactivatedUserPassword string `json:"TestDeactivatedUserPassword"` } // ProxyEnv returns a set of environment variable strings (e.g., to combine with os.Environ()) which set up the configured test HTTP proxy. @@ -274,24 +275,25 @@ func loadEnvVars(t *testing.T, result *TestEnv) { } result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{ - Host: wantEnv("PINNIPED_TEST_AD_HOST", ""), - CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")), - BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""), - BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""), - TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), - TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), - TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), - TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), - TestUserMailAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_VALUE", ""), - TestUserMailAttributeName: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_NAME", ""), - TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), - TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), - TestUserIndirectGroupsSAMAccountNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME", ""), ";")), - TestDeactivatedUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_SAMACCOUNTNAME", ""), - TestDeactivatedUserPassword: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_PASSWORD", ""), - DefaultNamingContextSearchBase: wantEnv("PINNIPED_TEST_AD_DEFAULTNAMINGCONTEXT_DN", ""), - UserSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""), - GroupSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""), + Host: wantEnv("PINNIPED_TEST_AD_HOST", ""), + CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")), + BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""), + BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""), + TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), + TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), + TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), + TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserMailAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_VALUE", ""), + TestUserMailAttributeName: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_NAME", ""), + TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), + TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), + TestUserIndirectGroupsSAMAccountNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME", ""), ";")), + TestUserIndirectGroupsSAMAccountPlusDomainNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME_DOMAINNAMES", ""), ";")), + TestDeactivatedUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_SAMACCOUNTNAME", ""), + TestDeactivatedUserPassword: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_PASSWORD", ""), + DefaultNamingContextSearchBase: wantEnv("PINNIPED_TEST_AD_DEFAULTNAMINGCONTEXT_DN", ""), + UserSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""), + GroupSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""), } sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) From 8657b0e3e7aedd1d5a7636cc4a71b800ccdc2572 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 18 Aug 2021 10:11:18 -0700 Subject: [PATCH 28/36] Cleanup new group attribute behavior and add test coverage --- .../active_directory_upstream_watcher.go | 13 +- .../active_directory_upstream_watcher_test.go | 75 ++--------- internal/upstreamldap/upstreamldap.go | 31 ++++- internal/upstreamldap/upstreamldap_test.go | 121 +++++++++++++++++- 4 files changed, 160 insertions(+), 80 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index b83b89100..abaeda98c 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -32,13 +32,12 @@ const ( defaultActiveDirectoryUsernameAttributeName = "userPrincipalName" defaultActiveDirectoryUIDAttributeName = "objectGUID" - // This is not a real attribute in active directory. - // It represents a combined attribute name, sAMAccountName + "@" + domain. + // By default this group name attribute is the sAMAccountName with special mapping. + // Each group will look like sAMAccountName + "@" + domain. // For example if your group sAMAccountName is "mammals" and your domain is // "activedirectory.example.com", it would be mammals@activedirectory.example.com. // This is because sAMAccountName is only unique within a domain, not a forest. - defaultActiveDirectoryGroupNameAttributeName = "sAMAccountName" - defaultActiveDirectoryGroupNameOverrideAttributeName = "pinniped:sAMAccountName@domain" + defaultActiveDirectoryGroupNameAttributeName = "sAMAccountName" // - is a person. // - is not a computer. @@ -188,10 +187,6 @@ func (g *activeDirectoryUpstreamGenericLDAPGroupSearch) GroupNameAttribute() str if len(g.groupSearch.Attributes.GroupName) == 0 { return defaultActiveDirectoryGroupNameAttributeName } - // you explicitly told us to use the override value - if g.groupSearch.Attributes.GroupName == defaultActiveDirectoryGroupNameOverrideAttributeName { - return defaultActiveDirectoryGroupNameAttributeName - } return g.groupSearch.Attributes.GroupName } @@ -321,7 +316,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, } - if spec.GroupSearch.Attributes.GroupName == defaultActiveDirectoryGroupNameOverrideAttributeName || spec.GroupSearch.Attributes.GroupName == "" { + if spec.GroupSearch.Attributes.GroupName == "" { config.GroupAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{{AttributeName: defaultActiveDirectoryGroupNameAttributeName, OverrideFunc: upstreamldap.GroupSAMAccountNameWithDomainSuffix}} } diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 289e0d2f5..2f5cada9d 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -218,7 +218,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, } // Make a copy with targeted changes. @@ -533,7 +533,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -591,7 +591,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -649,7 +649,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), @@ -706,7 +706,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -830,7 +830,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -950,7 +950,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1000,7 +1000,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1190,58 +1190,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))", GroupNameAttribute: "sAMAccountName", }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, - GroupAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "sAMAccountName", OverrideFunc: upstreamldap.GroupSAMAccountNameWithDomainSuffix}}, - }, - }, - wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ - ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, - Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ - Phase: "Ready", - Conditions: allConditionsTrue(1234, "4242"), - }, - }}, - wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: { - BindSecretResourceVersion: "4242", - LDAPConnectionProtocol: upstreamldap.TLS, - UserSearchBase: testUserSearchBase, - GroupSearchBase: testGroupSearchBase, - }}, - }, - { - name: "when the input activedirectoryidentityprovider group search attributes is the special cased pinniped:sAMAccountName@domain value", - inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { - upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} - upstream.Spec.UserSearch.Filter = "" - upstream.Spec.GroupSearch.Filter = "" - upstream.Spec.GroupSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{GroupName: "pinniped:sAMAccountName@domain"} - })}, - inputSecrets: []runtime.Object{validBindUserSecret("4242")}, - 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, - Host: testHost, - ConnectionProtocol: upstreamldap.TLS, - CABundle: testCABundle, - BindUsername: testBindUsername, - BindPassword: testBindPassword, - UserSearch: upstreamldap.UserSearchConfig{ - Base: testUserSearchBase, - Filter: "(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={})(mail={})(userPrincipalName={}))(sAMAccountType=805306368))", - UsernameAttribute: "userPrincipalName", - UIDAttribute: "objectGUID", - }, - GroupSearch: upstreamldap.GroupSearchConfig{ - Base: testGroupSearchBase, - Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))", - GroupNameAttribute: "sAMAccountName", - }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, GroupAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "sAMAccountName", OverrideFunc: upstreamldap.GroupSAMAccountNameWithDomainSuffix}}, }, }, @@ -1292,7 +1241,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1341,7 +1290,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1390,7 +1339,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary}}, + UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index b6e9571a6..37dab4a87 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -386,23 +386,26 @@ func (p *Provider) searchGroupsForUserDN(conn Conn, userDN string) ([]string, er } groups := []string{} +entries: for _, groupEntry := range searchResult.Entries { if len(groupEntry.DN) == 0 { return nil, fmt.Errorf(`searching for group memberships for user with DN %q resulted in search result without DN`, userDN) } - mappedGroupName, err := p.getSearchResultAttributeValue(groupAttributeName, groupEntry, userDN) - if err != nil { - return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err) - } for _, override := range p.c.GroupAttributeParsingOverrides { if groupAttributeName == override.AttributeName { overrideGroupName, err := override.OverrideFunc(groupEntry) if err != nil { - return nil, fmt.Errorf("error finding groups: %w", err) + return nil, fmt.Errorf("error finding groups for user %s: %w", userDN, err) } - mappedGroupName = overrideGroupName + groups = append(groups, overrideGroupName) + continue entries } } + // if none of the overrides matched, use the default behavior (no mapping) + mappedGroupName, err := p.getSearchResultAttributeValue(groupAttributeName, groupEntry, userDN) + if err != nil { + return nil, fmt.Errorf(`error searching for group memberships for user with DN %q: %w`, userDN, err) + } groups = append(groups, mappedGroupName) } @@ -708,7 +711,21 @@ func microsoftUUIDFromBinary(binaryUUID []byte) (string, error) { func GroupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) { sAMAccountNameAttribute := "sAMAccountName" - sAMAccountName := entry.GetAttributeValue(sAMAccountNameAttribute) + sAMAccountNameAttributeValues := entry.GetAttributeValues(sAMAccountNameAttribute) + + if len(sAMAccountNameAttributeValues) != 1 { + return "", fmt.Errorf(`found %d values for attribute "%s", but expected 1 result`, + len(sAMAccountNameAttributeValues), sAMAccountNameAttribute, + ) + } + + sAMAccountName := sAMAccountNameAttributeValues[0] + if len(sAMAccountName) == 0 { + return "", fmt.Errorf(`found empty value for attribute "%s", but expected value to be non-empty`, + sAMAccountNameAttribute, + ) + } + distinguishedName := entry.DN domain, err := getDomainFromDistinguishedName(distinguishedName) if err != nil { diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 641bee40c..59804de37 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -608,6 +608,62 @@ func TestEndUserAuthentication(t *testing.T) { r.Groups = []string{"Animals@activedirectory.mycompany.example.com", "Mammals@activedirectory.mycompany.example.com"} }), }, + { + name: "only the first group override for a given attribute name is applied", + // the choice to only run the first is somewhat arbitrary and likely irrelevant since we only + // have one group override at the moment... + // And as soon as we have starlark attribute mapping it will be obsolete. But this test + // ensures that we + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "sAMAccountName" + p.GroupAttributeParsingOverrides = []AttributeParsingOverride{ + { + AttributeName: "sAMAccountName", + OverrideFunc: GroupSAMAccountNameWithDomainSuffix, + }, + { + AttributeName: "sAMAccountName", + OverrideFunc: func(entry *ldap.Entry) (string, error) { + return "override-group-name", nil + }, + }, + } + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{"sAMAccountName"} + }), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}), + }, + }, + { + DN: "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Animals"}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Groups = []string{"Animals@activedirectory.mycompany.example.com", "Mammals@activedirectory.mycompany.example.com"} + }), + }, { name: "override group parsing when domain can't be determined from dn", username: testUpstreamUsername, @@ -645,7 +701,70 @@ func TestEndUserAuthentication(t *testing.T) { }, nil).Times(1) conn.EXPECT().Close().Times(1) }, - wantError: "error finding groups: did not find domain components in group dn: no-domain-components", + wantError: "error finding groups for user some-upstream-user-dn: did not find domain components in group dn: no-domain-components", + }, + { + name: "override group parsing when entry has multiple values for attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "sAMAccountName" + p.GroupAttributeParsingOverrides = []AttributeParsingOverride{{ + AttributeName: "sAMAccountName", + OverrideFunc: GroupSAMAccountNameWithDomainSuffix, + }} + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{"sAMAccountName"} + }), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "no-domain-components", + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals", "Eukaryotes"}), + }, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: "error finding groups for user some-upstream-user-dn: found 2 values for attribute \"sAMAccountName\", but expected 1 result", + }, { + name: "override group parsing when entry has no values for attribute", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "sAMAccountName" + p.GroupAttributeParsingOverrides = []AttributeParsingOverride{{ + AttributeName: "sAMAccountName", + OverrideFunc: GroupSAMAccountNameWithDomainSuffix, + }} + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{"sAMAccountName"} + }), expectedGroupSearchPageSize). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "no-domain-components", + Attributes: []*ldap.EntryAttribute{}, + }, + }, + Referrals: []string{}, // note that we are not following referrals at this time + Controls: []ldap.Control{}, + }, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantError: "error finding groups for user some-upstream-user-dn: found 0 values for attribute \"sAMAccountName\", but expected 1 result", }, { name: "when dial fails", From 90e6298e293a028a3ecb5a15ff4246e8b69d57a9 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 18 Aug 2021 10:39:01 -0700 Subject: [PATCH 29/36] Update text on CRD templates to reflect new defaults --- .../types_activedirectoryidentityprovider.go.tmpl | 8 ++++---- ...sor.pinniped.dev_activedirectoryidentityproviders.yaml | 8 ++++---- generated/1.17/README.adoc | 8 ++++---- .../idp/v1alpha1/types_activedirectoryidentityprovider.go | 8 ++++---- ...sor.pinniped.dev_activedirectoryidentityproviders.yaml | 8 ++++---- generated/1.18/README.adoc | 8 ++++---- .../idp/v1alpha1/types_activedirectoryidentityprovider.go | 8 ++++---- ...sor.pinniped.dev_activedirectoryidentityproviders.yaml | 8 ++++---- generated/1.19/README.adoc | 8 ++++---- .../idp/v1alpha1/types_activedirectoryidentityprovider.go | 8 ++++---- ...sor.pinniped.dev_activedirectoryidentityproviders.yaml | 8 ++++---- generated/1.20/README.adoc | 8 ++++---- .../idp/v1alpha1/types_activedirectoryidentityprovider.go | 8 ++++---- ...sor.pinniped.dev_activedirectoryidentityproviders.yaml | 8 ++++---- .../idp/v1alpha1/types_activedirectoryidentityprovider.go | 8 ++++---- go.mod | 2 +- 16 files changed, 61 insertions(+), 61 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl index 791e47a6d..e8e9fb269 100644 --- a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl @@ -48,7 +48,7 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "sAMAccountName". + // Optional, when empty this defaults to "userPrincipalName". // +optional Username string `json:"username,omitempty"` @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to "sAMAccountName". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". // +optional GroupName string `json:"groupName,omitempty"` } @@ -82,7 +82,7 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be - // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -105,7 +105,7 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 + // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. // +optional diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 486590530..83eb8d2a5 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,7 +89,7 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to "sAMAccountName". + to a custom field that looks like "sAMAccountName@domain". type: string type: object base: @@ -108,7 +108,7 @@ spec: about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + the default will act as if the filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. type: string type: object @@ -147,7 +147,7 @@ spec: in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when - empty this defaults to "sAMAccountName". + empty this defaults to "userPrincipalName". type: string type: object base: @@ -165,7 +165,7 @@ spec: 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 9cce14769..fbd14cf3c 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -799,7 +799,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to "sAMAccountName". +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". |=== @@ -876,7 +876,7 @@ Status of an Active Directory identity provider. |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "userPrincipalName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 791e47a6d..e8e9fb269 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -48,7 +48,7 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "sAMAccountName". + // Optional, when empty this defaults to "userPrincipalName". // +optional Username string `json:"username,omitempty"` @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to "sAMAccountName". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". // +optional GroupName string `json:"groupName,omitempty"` } @@ -82,7 +82,7 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be - // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -105,7 +105,7 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 + // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. // +optional diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 486590530..83eb8d2a5 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,7 +89,7 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to "sAMAccountName". + to a custom field that looks like "sAMAccountName@domain". type: string type: object base: @@ -108,7 +108,7 @@ spec: about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + the default will act as if the filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. type: string type: object @@ -147,7 +147,7 @@ spec: in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when - empty this defaults to "sAMAccountName". + empty this defaults to "userPrincipalName". type: string type: object base: @@ -165,7 +165,7 @@ spec: 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index ed0623c6b..b3f341b96 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -799,7 +799,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to "sAMAccountName". +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". |=== @@ -876,7 +876,7 @@ Status of an Active Directory identity provider. |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "userPrincipalName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 791e47a6d..e8e9fb269 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -48,7 +48,7 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "sAMAccountName". + // Optional, when empty this defaults to "userPrincipalName". // +optional Username string `json:"username,omitempty"` @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to "sAMAccountName". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". // +optional GroupName string `json:"groupName,omitempty"` } @@ -82,7 +82,7 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be - // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -105,7 +105,7 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 + // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. // +optional diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 486590530..83eb8d2a5 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,7 +89,7 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to "sAMAccountName". + to a custom field that looks like "sAMAccountName@domain". type: string type: object base: @@ -108,7 +108,7 @@ spec: about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + the default will act as if the filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. type: string type: object @@ -147,7 +147,7 @@ spec: in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when - empty this defaults to "sAMAccountName". + empty this defaults to "userPrincipalName". type: string type: object base: @@ -165,7 +165,7 @@ spec: 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 45191ade8..3fd892a10 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -799,7 +799,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to "sAMAccountName". +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". |=== @@ -876,7 +876,7 @@ Status of an Active Directory identity provider. |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "userPrincipalName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 791e47a6d..e8e9fb269 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -48,7 +48,7 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "sAMAccountName". + // Optional, when empty this defaults to "userPrincipalName". // +optional Username string `json:"username,omitempty"` @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to "sAMAccountName". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". // +optional GroupName string `json:"groupName,omitempty"` } @@ -82,7 +82,7 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be - // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -105,7 +105,7 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 + // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. // +optional diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 486590530..83eb8d2a5 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,7 +89,7 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to "sAMAccountName". + to a custom field that looks like "sAMAccountName@domain". type: string type: object base: @@ -108,7 +108,7 @@ spec: about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + the default will act as if the filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. type: string type: object @@ -147,7 +147,7 @@ spec: in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when - empty this defaults to "sAMAccountName". + empty this defaults to "userPrincipalName". type: string type: object base: @@ -165,7 +165,7 @@ spec: 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 146d458be..1e7060a2e 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -799,7 +799,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to "sAMAccountName". +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". |=== @@ -876,7 +876,7 @@ Status of an Active Directory identity provider. |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "sAMAccountName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "userPrincipalName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 791e47a6d..e8e9fb269 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -48,7 +48,7 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "sAMAccountName". + // Optional, when empty this defaults to "userPrincipalName". // +optional Username string `json:"username,omitempty"` @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to "sAMAccountName". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". // +optional GroupName string `json:"groupName,omitempty"` } @@ -82,7 +82,7 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be - // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -105,7 +105,7 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 + // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. // +optional diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 486590530..83eb8d2a5 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,7 +89,7 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to "sAMAccountName". + to a custom field that looks like "sAMAccountName@domain". type: string type: object base: @@ -108,7 +108,7 @@ spec: about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". + the default will act as if the filter were specified as "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. type: string type: object @@ -147,7 +147,7 @@ spec: in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when - empty this defaults to "sAMAccountName". + empty this defaults to "userPrincipalName". type: string type: object base: @@ -165,7 +165,7 @@ spec: 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' type: string type: object required: diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 791e47a6d..e8e9fb269 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -48,7 +48,7 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "sAMAccountName". + // Optional, when empty this defaults to "userPrincipalName". // +optional Username string `json:"username,omitempty"` @@ -64,7 +64,7 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to "sAMAccountName". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". // +optional GroupName string `json:"groupName,omitempty"` } @@ -82,7 +82,7 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be - // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={}))(sAMAccountType=805306368))' + // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' // +optional Filter string `json:"filter,omitempty"` @@ -105,7 +105,7 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 + // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. // +optional diff --git a/go.mod b/go.mod index e333acf5f..98d4adea4 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.6 github.com/google/gofuzz v1.2.0 - github.com/google/uuid v1.1.2 // indirect + github.com/google/uuid v1.1.2 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/websocket v1.4.2 github.com/onsi/ginkgo v1.13.0 // indirect From 1d18908055cf67067e4954eebbbb95e15709ea28 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 18 Aug 2021 12:54:01 -0700 Subject: [PATCH 30/36] Fix test error-- execcredential now has interactive:false for activedirectoryidentityprovider test, which didn't exist on main when #770 was merged to update the other tests to use 1.22. --- cmd/pinniped/cmd/login_oidc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index f7be0dab4..f140710a6 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -182,7 +182,7 @@ func TestLoginOIDCCommand(t *testing.T) { "--credential-cache", "", // must specify --credential-cache or else the cache file on disk causes test pollution }, wantOptionsCount: 5, - wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", + wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"3020-10-12T13:14:15Z","token":"test-id-token"}}` + "\n", }, { name: "login error", From a20aee5f18aac1770ee17736d1f21058e8df11ae Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 18 Aug 2021 13:18:53 -0700 Subject: [PATCH 31/36] Update test assertions to reflect userPrincipalName as username --- test/integration/supervisor_login_test.go | 6 +++--- test/testlib/env.go | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 1ff736218..e5eb146f3 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -331,8 +331,8 @@ func TestSupervisorLogin(t *testing.T) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorizationUsingLDAPIdentityProvider(t, downstreamAuthorizeURL, - env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue, // username to present to server during login - env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login httpClient, false, ) @@ -344,7 +344,7 @@ func TestSupervisorLogin(t *testing.T) { "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue), + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, }, { name: "activedirectory with custom options", diff --git a/test/testlib/env.go b/test/testlib/env.go index f9ea824c0..9caf9be53 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -100,6 +100,7 @@ type TestLDAPUpstream struct { TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` TestUserDirectGroupsDNs []string `json:"testUserDirectGroupsDNs"` //nolint:golint // this is "distinguished names", not "DNS" TestUserSAMAccountNameValue string `json:"testUserSAMAccountNameValue"` + TestUserPrincipalNameValue string `json:"testUserPrincipalNameValue"` TestUserIndirectGroupsSAMAccountNames []string `json:"TestUserIndirectGroupsSAMAccountNames"` TestUserIndirectGroupsSAMAccountPlusDomainNames []string `json:"TestUserIndirectGroupsSAMAccountPlusDomainNames"` TestDeactivatedUserSAMAccountNameValue string `json:"TestDeactivatedUserSAMAccountNameValue"` @@ -282,7 +283,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), - TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserPrincipalNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), TestUserMailAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_VALUE", ""), TestUserMailAttributeName: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_NAME", ""), TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), From 5e9087263d414950dd2ad05d53a116733b3fd710 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 18 Aug 2021 16:24:05 -0700 Subject: [PATCH 32/36] Increase timeout for activedirectoryidentityprovider to be loaded --- test/integration/supervisor_login_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index e5eb146f3..3bd837528 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -830,7 +830,7 @@ func requestAuthorizationUsingLDAPIdentityProvider(t *testing.T, downstreamAutho return false, nil } return true, nil - }, 30*time.Second, 200*time.Millisecond) + }, 60*time.Second, 200*time.Millisecond) expectSecurityHeaders(t, authResponse, true) From 05afae60c2d3dd95e805ea4a2899513493a1e31d Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 19 Aug 2021 14:21:18 -0700 Subject: [PATCH 33/36] Review comments-- - Change list of attributeParsingOverrides to a map - Add unit test for sAMAccountName as group name without the override - Change some comments in the the type definition. --- ...es_activedirectoryidentityprovider.go.tmpl | 9 +- ....dev_activedirectoryidentityproviders.yaml | 9 +- generated/1.17/README.adoc | 4 +- .../types_activedirectoryidentityprovider.go | 9 +- ....dev_activedirectoryidentityproviders.yaml | 9 +- generated/1.18/README.adoc | 4 +- .../types_activedirectoryidentityprovider.go | 9 +- ....dev_activedirectoryidentityproviders.yaml | 9 +- generated/1.19/README.adoc | 4 +- .../types_activedirectoryidentityprovider.go | 9 +- ....dev_activedirectoryidentityproviders.yaml | 9 +- generated/1.20/README.adoc | 4 +- .../types_activedirectoryidentityprovider.go | 9 +- ....dev_activedirectoryidentityproviders.yaml | 9 +- .../types_activedirectoryidentityprovider.go | 9 +- .../active_directory_upstream_watcher.go | 6 +- .../active_directory_upstream_watcher_test.go | 101 ++++++++++++++---- internal/upstreamldap/upstreamldap.go | 38 +++---- internal/upstreamldap/upstreamldap_test.go | 96 +++-------------- 19 files changed, 183 insertions(+), 173 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl index e8e9fb269..5889bef6b 100644 --- a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl @@ -47,9 +47,9 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username - // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "userPrincipalName". - // +optional + // of the user after a successful authentication. + // Optional, when empty this defaults to "userPrincipalName". + // +optional Username string `json:"username,omitempty"` // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely @@ -108,6 +108,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. + // Note that nested group search can be slow for some Active Directory servers. To disable it, + // you can set the filter to + // "(&(objectClass=group)(member={})" // +optional Filter string `json:"filter,omitempty"` diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 83eb8d2a5..87f37a021 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -109,7 +109,9 @@ spec: 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". - This searches nested groups by default. + This searches nested groups by default. Note that nested group + search can be slow for some Active Directory servers. To disable + it, you can set the filter to "(&(objectClass=group)(member={})" type: string type: object host: @@ -145,9 +147,8 @@ spec: username: description: Username specifies the name of the attribute in Active Directory entry whose value shall become the username - of the user after a successful authentication. This would - typically be the same attribute name used in Optional, when - empty this defaults to "userPrincipalName". + of the user after a successful authentication. Optional, + when empty this defaults to "userPrincipalName". type: string type: object base: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index fbd14cf3c..bd077fba5 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -799,7 +799,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "userPrincipalName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. Optional, when empty this defaults to "userPrincipalName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index e8e9fb269..5889bef6b 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -47,9 +47,9 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username - // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "userPrincipalName". - // +optional + // of the user after a successful authentication. + // Optional, when empty this defaults to "userPrincipalName". + // +optional Username string `json:"username,omitempty"` // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely @@ -108,6 +108,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. + // Note that nested group search can be slow for some Active Directory servers. To disable it, + // you can set the filter to + // "(&(objectClass=group)(member={})" // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 83eb8d2a5..87f37a021 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -109,7 +109,9 @@ spec: 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". - This searches nested groups by default. + This searches nested groups by default. Note that nested group + search can be slow for some Active Directory servers. To disable + it, you can set the filter to "(&(objectClass=group)(member={})" type: string type: object host: @@ -145,9 +147,8 @@ spec: username: description: Username specifies the name of the attribute in Active Directory entry whose value shall become the username - of the user after a successful authentication. This would - typically be the same attribute name used in Optional, when - empty this defaults to "userPrincipalName". + of the user after a successful authentication. Optional, + when empty this defaults to "userPrincipalName". type: string type: object base: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index b3f341b96..92630adee 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -799,7 +799,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "userPrincipalName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. Optional, when empty this defaults to "userPrincipalName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index e8e9fb269..5889bef6b 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -47,9 +47,9 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username - // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "userPrincipalName". - // +optional + // of the user after a successful authentication. + // Optional, when empty this defaults to "userPrincipalName". + // +optional Username string `json:"username,omitempty"` // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely @@ -108,6 +108,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. + // Note that nested group search can be slow for some Active Directory servers. To disable it, + // you can set the filter to + // "(&(objectClass=group)(member={})" // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 83eb8d2a5..87f37a021 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -109,7 +109,9 @@ spec: 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". - This searches nested groups by default. + This searches nested groups by default. Note that nested group + search can be slow for some Active Directory servers. To disable + it, you can set the filter to "(&(objectClass=group)(member={})" type: string type: object host: @@ -145,9 +147,8 @@ spec: username: description: Username specifies the name of the attribute in Active Directory entry whose value shall become the username - of the user after a successful authentication. This would - typically be the same attribute name used in Optional, when - empty this defaults to "userPrincipalName". + of the user after a successful authentication. Optional, + when empty this defaults to "userPrincipalName". type: string type: object base: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 3fd892a10..4985760cf 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -799,7 +799,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "userPrincipalName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. Optional, when empty this defaults to "userPrincipalName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index e8e9fb269..5889bef6b 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -47,9 +47,9 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username - // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "userPrincipalName". - // +optional + // of the user after a successful authentication. + // Optional, when empty this defaults to "userPrincipalName". + // +optional Username string `json:"username,omitempty"` // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely @@ -108,6 +108,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. + // Note that nested group search can be slow for some Active Directory servers. To disable it, + // you can set the filter to + // "(&(objectClass=group)(member={})" // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 83eb8d2a5..87f37a021 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -109,7 +109,9 @@ spec: 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". - This searches nested groups by default. + This searches nested groups by default. Note that nested group + search can be slow for some Active Directory servers. To disable + it, you can set the filter to "(&(objectClass=group)(member={})" type: string type: object host: @@ -145,9 +147,8 @@ spec: username: description: Username specifies the name of the attribute in Active Directory entry whose value shall become the username - of the user after a successful authentication. This would - typically be the same attribute name used in Optional, when - empty this defaults to "userPrincipalName". + of the user after a successful authentication. Optional, + when empty this defaults to "userPrincipalName". type: string type: object base: diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 1e7060a2e..40d1db7c7 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -799,7 +799,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro |=== | Field | Description | *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. +| *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -894,7 +894,7 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in Optional, when empty this defaults to "userPrincipalName". +| *`username`* __string__ | Username specifies the name of the attribute in Active Directory entry whose value shall become the username of the user after a successful authentication. Optional, when empty this defaults to "userPrincipalName". | *`uid`* __string__ | UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely identify the user within this ActiveDirectory provider after a successful authentication. Optional, when empty this defaults to "objectGUID". |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index e8e9fb269..5889bef6b 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -47,9 +47,9 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username - // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "userPrincipalName". - // +optional + // of the user after a successful authentication. + // Optional, when empty this defaults to "userPrincipalName". + // +optional Username string `json:"username,omitempty"` // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely @@ -108,6 +108,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. + // Note that nested group search can be slow for some Active Directory servers. To disable it, + // you can set the filter to + // "(&(objectClass=group)(member={})" // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 83eb8d2a5..87f37a021 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -109,7 +109,9 @@ spec: 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". - This searches nested groups by default. + This searches nested groups by default. Note that nested group + search can be slow for some Active Directory servers. To disable + it, you can set the filter to "(&(objectClass=group)(member={})" type: string type: object host: @@ -145,9 +147,8 @@ spec: username: description: Username specifies the name of the attribute in Active Directory entry whose value shall become the username - of the user after a successful authentication. This would - typically be the same attribute name used in Optional, when - empty this defaults to "userPrincipalName". + of the user after a successful authentication. Optional, + when empty this defaults to "userPrincipalName". type: string type: object base: diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index e8e9fb269..5889bef6b 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -47,9 +47,9 @@ type ActiveDirectoryIdentityProviderBind struct { type ActiveDirectoryIdentityProviderUserSearchAttributes struct { // Username specifies the name of the attribute in Active Directory entry whose value shall become the username - // of the user after a successful authentication. This would typically be the same attribute name used in - // Optional, when empty this defaults to "userPrincipalName". - // +optional + // of the user after a successful authentication. + // Optional, when empty this defaults to "userPrincipalName". + // +optional Username string `json:"username,omitempty"` // UID specifies the name of the attribute in the ActiveDirectory entry which whose value shall be used to uniquely @@ -108,6 +108,9 @@ type ActiveDirectoryIdentityProviderGroupSearch struct { // Optional. When not specified, the default will act as if the filter were specified as // "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". // This searches nested groups by default. + // Note that nested group search can be slow for some Active Directory servers. To disable it, + // you can set the filter to + // "(&(objectClass=group)(member={})" // +optional Filter string `json:"filter,omitempty"` diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index abaeda98c..c32793d9f 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -8,6 +8,8 @@ import ( "context" "fmt" + "github.com/go-ldap/ldap/v3" + "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -313,11 +315,11 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, GroupNameAttribute: adUpstreamImpl.Spec().GroupSearch().GroupNameAttribute(), }, Dialer: c.ldapDialer, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, } if spec.GroupSearch.Attributes.GroupName == "" { - config.GroupAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{{AttributeName: defaultActiveDirectoryGroupNameAttributeName, OverrideFunc: upstreamldap.GroupSAMAccountNameWithDomainSuffix}} + config.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){defaultActiveDirectoryGroupNameAttributeName: upstreamldap.GroupSAMAccountNameWithDomainSuffix} } conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 2f5cada9d..a2326ac2c 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -218,7 +218,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, } // Make a copy with targeted changes. @@ -533,7 +533,62 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, + }, + }, + wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, + Status: v1alpha1.ActiveDirectoryIdentityProviderStatus{ + Phase: "Ready", + Conditions: []v1alpha1.Condition{ + bindSecretValidTrueCondition(1234), + activeDirectoryConnectionValidTrueCondition(1234, "4242"), + searchBaseFoundInConfigCondition(1234), + { + Type: "TLSConfigurationValid", + Status: "True", + LastTransitionTime: now, + Reason: "Success", + Message: "no TLS configuration provided", + ObservedGeneration: 1234, + }, + }, + }, + }}, + wantValidatedSettings: map[string]upstreamwatchers.ValidatedSettings{testName: {BindSecretResourceVersion: "4242", LDAPConnectionProtocol: upstreamldap.TLS, UserSearchBase: testUserSearchBase, GroupSearchBase: testGroupSearchBase}}, + }, + { + name: "sAMAccountName explicitly provided as group name attribute does not add an override", + inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { + upstream.Spec.TLS = nil + upstream.Spec.GroupSearch.Attributes.GroupName = "sAMAccountName" + })}, + inputSecrets: []runtime.Object{validBindUserSecret("4242")}, + 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, + Host: testHost, + ConnectionProtocol: upstreamldap.TLS, + CABundle: nil, + BindUsername: testBindUsername, + BindPassword: testBindPassword, + UserSearch: upstreamldap.UserSearchConfig{ + Base: testUserSearchBase, + Filter: testUserSearchFilter, + UsernameAttribute: testUsernameAttrName, + UIDAttribute: testUIDAttrName, + }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: "sAMAccountName", + }, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -591,7 +646,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -649,7 +704,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantErr: controllerlib.ErrSyntheticRequeue.Error(), @@ -706,7 +761,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -830,7 +885,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -950,7 +1005,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1000,7 +1055,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1190,8 +1245,8 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={}))", GroupNameAttribute: "sAMAccountName", }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, - GroupAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "sAMAccountName", OverrideFunc: upstreamldap.GroupSAMAccountNameWithDomainSuffix}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, + GroupAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"sAMAccountName": upstreamldap.GroupSAMAccountNameWithDomainSuffix}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1241,7 +1296,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1290,7 +1345,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1339,7 +1394,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Filter: testGroupSearchFilter, GroupNameAttribute: testGroupNameAttrName, }, - UIDAttributeParsingOverrides: []upstreamldap.AttributeParsingOverride{{AttributeName: "objectGUID", OverrideFunc: upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}}, + UIDAttributeParsingOverrides: map[string]func(*ldap.Entry) (string, error){"objectGUID": upstreamldap.MicrosoftUUIDFromBinary("objectGUID")}, }, }, wantResultingUpstreams: []v1alpha1.ActiveDirectoryIdentityProvider{{ @@ -1580,25 +1635,25 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { expectedUIDAttributeParsingOverrides := copyOfExpectedValueForResultingCache.UIDAttributeParsingOverrides actualConfig := actualIDP.GetConfig() actualUIDAttributeParsingOverrides := actualConfig.UIDAttributeParsingOverrides - copyOfExpectedValueForResultingCache.UIDAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} - actualConfig.UIDAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} + copyOfExpectedValueForResultingCache.UIDAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){} + actualConfig.UIDAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){} require.Equal(t, len(expectedUIDAttributeParsingOverrides), len(actualUIDAttributeParsingOverrides)) - for i := range expectedUIDAttributeParsingOverrides { - require.Equal(t, expectedUIDAttributeParsingOverrides[i].AttributeName, actualUIDAttributeParsingOverrides[i].AttributeName) - require.Equal(t, reflect.ValueOf(expectedUIDAttributeParsingOverrides[i].OverrideFunc).Pointer(), reflect.ValueOf(actualUIDAttributeParsingOverrides[i].OverrideFunc).Pointer()) + for k, v := range expectedUIDAttributeParsingOverrides { + require.NotNil(t, actualUIDAttributeParsingOverrides[k]) + require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualUIDAttributeParsingOverrides[k]).Pointer()) } // function equality is awkward. Do the check for equality separately from the rest of the config. expectedGroupAttributeParsingOverrides := copyOfExpectedValueForResultingCache.GroupAttributeParsingOverrides actualGroupAttributeParsingOverrides := actualConfig.GroupAttributeParsingOverrides - copyOfExpectedValueForResultingCache.GroupAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} - actualConfig.GroupAttributeParsingOverrides = []upstreamldap.AttributeParsingOverride{} + copyOfExpectedValueForResultingCache.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){} + actualConfig.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){} require.Equal(t, len(expectedGroupAttributeParsingOverrides), len(actualGroupAttributeParsingOverrides)) - for i := range expectedGroupAttributeParsingOverrides { - require.Equal(t, expectedGroupAttributeParsingOverrides[i].AttributeName, actualGroupAttributeParsingOverrides[i].AttributeName) - require.Equal(t, reflect.ValueOf(expectedGroupAttributeParsingOverrides[i].OverrideFunc).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[i].OverrideFunc).Pointer()) + for k, v := range expectedGroupAttributeParsingOverrides { + require.NotNil(t, actualGroupAttributeParsingOverrides[k]) + require.Equal(t, reflect.ValueOf(v).Pointer(), reflect.ValueOf(actualGroupAttributeParsingOverrides[k]).Pointer()) } require.Equal(t, copyOfExpectedValueForResultingCache, actualConfig) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 37dab4a87..1a147b43d 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -18,9 +18,8 @@ import ( "strings" "time" - "github.com/google/uuid" - "github.com/go-ldap/ldap/v3" + "github.com/google/uuid" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/utils/trace" @@ -38,6 +37,7 @@ const ( groupSearchPageSize = uint32(250) defaultLDAPPort = uint16(389) defaultLDAPSPort = uint16(636) + sAMAccountNameAttribute = "sAMAccountName" ) // Conn abstracts the upstream LDAP communication protocol (mostly for testing). @@ -109,16 +109,11 @@ type ProviderConfig struct { // UIDAttributeParsingOverrides are mappings between an attribute name and a way to parse it as a UID when // it comes out of LDAP. - UIDAttributeParsingOverrides []AttributeParsingOverride + UIDAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error) // GroupNameMappingOverrides are the mappings between an attribute name and a way to parse it as a group // name when it comes out of LDAP. - GroupAttributeParsingOverrides []AttributeParsingOverride -} - -type AttributeParsingOverride struct { - AttributeName string - OverrideFunc func(entry *ldap.Entry) (string, error) + GroupAttributeParsingOverrides map[string]func(*ldap.Entry) (string, error) } // UserSearchConfig contains information about how to search for users in the upstream LDAP IDP. @@ -391,15 +386,13 @@ entries: if len(groupEntry.DN) == 0 { return nil, fmt.Errorf(`searching for group memberships for user with DN %q resulted in search result without DN`, userDN) } - for _, override := range p.c.GroupAttributeParsingOverrides { - if groupAttributeName == override.AttributeName { - overrideGroupName, err := override.OverrideFunc(groupEntry) - if err != nil { - return nil, fmt.Errorf("error finding groups for user %s: %w", userDN, err) - } - groups = append(groups, overrideGroupName) - continue entries + if overrideFunc := p.c.GroupAttributeParsingOverrides[groupAttributeName]; overrideFunc != nil { + overrideGroupName, err := overrideFunc(groupEntry) + if err != nil { + return nil, fmt.Errorf("error finding groups for user %s: %w", userDN, err) } + groups = append(groups, overrideGroupName) + continue entries } // if none of the overrides matched, use the default behavior (no mapping) mappedGroupName, err := p.getSearchResultAttributeValue(groupAttributeName, groupEntry, userDN) @@ -638,10 +631,8 @@ func (p *Provider) getSearchResultAttributeRawValueEncoded(attributeName string, ) } - for _, override := range p.c.UIDAttributeParsingOverrides { - if attributeName == override.AttributeName { - return override.OverrideFunc(entry) - } + if overrideFunc := p.c.UIDAttributeParsingOverrides[attributeName]; overrideFunc != nil { + return overrideFunc(entry) } return base64.RawURLEncoding.EncodeToString(attributeValue), nil @@ -710,7 +701,6 @@ func microsoftUUIDFromBinary(binaryUUID []byte) (string, error) { } func GroupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) { - sAMAccountNameAttribute := "sAMAccountName" sAMAccountNameAttributeValues := entry.GetAttributeValues(sAMAccountNameAttribute) if len(sAMAccountNameAttributeValues) != 1 { @@ -734,8 +724,10 @@ func GroupSAMAccountNameWithDomainSuffix(entry *ldap.Entry) (string, error) { return sAMAccountName + "@" + domain, nil } +var domainComponentsRegexp = regexp.MustCompile(",DC=|,dc=") + func getDomainFromDistinguishedName(distinguishedName string) (string, error) { - domainComponents := regexp.MustCompile(",DC=|,dc=").Split(distinguishedName, -1) + domainComponents := domainComponentsRegexp.Split(distinguishedName, -1) if len(domainComponents) == 1 { return "", fmt.Errorf("did not find domain components in group dn: %s", distinguishedName) } diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 59804de37..0cb1f355c 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -511,10 +511,9 @@ func TestEndUserAuthentication(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { - p.UIDAttributeParsingOverrides = []AttributeParsingOverride{{ - AttributeName: "objectGUID", - OverrideFunc: MicrosoftUUIDFromBinary("objectGUID"), - }} + p.UIDAttributeParsingOverrides = map[string]func(entry *ldap.Entry) (string, error){ + "objectGUID": MicrosoftUUIDFromBinary("objectGUID"), + } p.UserSearch.UIDAttribute = "objectGUID" }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -547,10 +546,9 @@ func TestEndUserAuthentication(t *testing.T) { username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { - p.UIDAttributeParsingOverrides = []AttributeParsingOverride{{ - AttributeName: "objectGUID", - OverrideFunc: MicrosoftUUIDFromBinary("objectGUID"), - }} + p.UIDAttributeParsingOverrides = map[string]func(entry *ldap.Entry) (string, error){ + "objectGUID": MicrosoftUUIDFromBinary("objectGUID"), + } }), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -570,65 +568,8 @@ func TestEndUserAuthentication(t *testing.T) { password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { p.GroupSearch.GroupNameAttribute = "sAMAccountName" - p.GroupAttributeParsingOverrides = []AttributeParsingOverride{{ - AttributeName: "sAMAccountName", - OverrideFunc: GroupSAMAccountNameWithDomainSuffix, - }} - }), - searchMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) - conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { - r.Attributes = []string{"sAMAccountName"} - }), expectedGroupSearchPageSize). - Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com", - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute("sAMAccountName", []string{"Mammals"}), - }, - }, - { - DN: "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com", - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute("sAMAccountName", []string{"Animals"}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, - }, nil).Times(1) - conn.EXPECT().Close().Times(1) - }, - bindEndUserMocks: func(conn *mockldapconn.MockConn) { - conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { - r.Groups = []string{"Animals@activedirectory.mycompany.example.com", "Mammals@activedirectory.mycompany.example.com"} - }), - }, - { - name: "only the first group override for a given attribute name is applied", - // the choice to only run the first is somewhat arbitrary and likely irrelevant since we only - // have one group override at the moment... - // And as soon as we have starlark attribute mapping it will be obsolete. But this test - // ensures that we - username: testUpstreamUsername, - password: testUpstreamPassword, - providerConfig: providerConfig(func(p *ProviderConfig) { - p.GroupSearch.GroupNameAttribute = "sAMAccountName" - p.GroupAttributeParsingOverrides = []AttributeParsingOverride{ - { - AttributeName: "sAMAccountName", - OverrideFunc: GroupSAMAccountNameWithDomainSuffix, - }, - { - AttributeName: "sAMAccountName", - OverrideFunc: func(entry *ldap.Entry) (string, error) { - return "override-group-name", nil - }, - }, + p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){ + "sAMAccountName": GroupSAMAccountNameWithDomainSuffix, } }), searchMocks: func(conn *mockldapconn.MockConn) { @@ -670,10 +611,9 @@ func TestEndUserAuthentication(t *testing.T) { password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { p.GroupSearch.GroupNameAttribute = "sAMAccountName" - p.GroupAttributeParsingOverrides = []AttributeParsingOverride{{ - AttributeName: "sAMAccountName", - OverrideFunc: GroupSAMAccountNameWithDomainSuffix, - }} + p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){ + "sAMAccountName": GroupSAMAccountNameWithDomainSuffix, + } }), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -709,10 +649,9 @@ func TestEndUserAuthentication(t *testing.T) { password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { p.GroupSearch.GroupNameAttribute = "sAMAccountName" - p.GroupAttributeParsingOverrides = []AttributeParsingOverride{{ - AttributeName: "sAMAccountName", - OverrideFunc: GroupSAMAccountNameWithDomainSuffix, - }} + p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){ + "sAMAccountName": GroupSAMAccountNameWithDomainSuffix, + } }), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -741,10 +680,9 @@ func TestEndUserAuthentication(t *testing.T) { password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { p.GroupSearch.GroupNameAttribute = "sAMAccountName" - p.GroupAttributeParsingOverrides = []AttributeParsingOverride{{ - AttributeName: "sAMAccountName", - OverrideFunc: GroupSAMAccountNameWithDomainSuffix, - }} + p.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){ + "sAMAccountName": GroupSAMAccountNameWithDomainSuffix, + } }), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) From 1c5a2b889294c5e83bf5896082a13da7117c91db Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Wed, 25 Aug 2021 11:33:42 -0700 Subject: [PATCH 34/36] Add a couple more unit tests --- internal/oidc/auth/auth_handler_test.go | 34 ++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index 1a39ad1fe..c0198ee18 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -946,7 +946,7 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "application/json; charset=utf-8", wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, - { // TODO maybe add one like this for AD + { name: "downstream redirect uri does not match what is configured for client when using LDAP upstream", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider), method: http.MethodGet, @@ -959,6 +959,19 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "application/json; charset=utf-8", wantBodyJSON: fositeInvalidRedirectURIErrorBody, }, + { + name: "downstream redirect uri does not match what is configured for client when using active directory upstream", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + 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: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), + wantStatus: http.StatusBadRequest, + wantContentType: "application/json; charset=utf-8", + wantBodyJSON: fositeInvalidRedirectURIErrorBody, + }, { name: "downstream client does not exist when using OIDC upstream browser flow", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), @@ -993,6 +1006,15 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "application/json; charset=utf-8", wantBodyJSON: fositeInvalidClientErrorBody, }, + { + name: "downstream client does not exist when using active directory upstream", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + 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 browser flow", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), @@ -1030,6 +1052,16 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeUnsupportedResponseTypeErrorQuery), wantBodyString: "", }, + { + name: "response type is unsupported when using active directory upstream", + idps: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider), + 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 browser flow", idps: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(upstreamOIDCIdentityProvider()), From 6f221678dfa22a2db22ddb1433f7c3beea2fc41e Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 26 Aug 2021 16:18:05 -0700 Subject: [PATCH 35/36] Change sAMAccountName env vars to userPrincipalName and add E2E ActiveDirectory test also fixed regexes in supervisor_login_test to be anchored to the beginning and end --- .../active_directory_upstream_watcher.go | 4 +- .../upstreamwatchers/upstream_watchers.go | 14 +- test/integration/e2e_test.go | 166 ++++++++++++++++++ test/integration/supervisor_login_test.go | 60 +++---- test/testlib/env.go | 2 +- 5 files changed, 208 insertions(+), 38 deletions(-) diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index c32793d9f..2c21d99dc 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -298,7 +298,7 @@ func (c *activeDirectoryWatcherController) Sync(ctx controllerlib.Context) error func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.ActiveDirectoryIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) { spec := upstream.Spec - adUpstreamImpl := activeDirectoryUpstreamGenericLDAPImpl{*upstream} + adUpstreamImpl := &activeDirectoryUpstreamGenericLDAPImpl{activeDirectoryIdentityProvider: *upstream} config := &upstreamldap.ProviderConfig{ Name: upstream.Name, @@ -322,7 +322,7 @@ func (c *activeDirectoryWatcherController) validateUpstream(ctx context.Context, config.GroupAttributeParsingOverrides = map[string]func(*ldap.Entry) (string, error){defaultActiveDirectoryGroupNameAttributeName: upstreamldap.GroupSAMAccountNameWithDomainSuffix} } - conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config) + conditions := upstreamwatchers.ValidateGenericLDAP(ctx, adUpstreamImpl, c.secretInformer, c.validatedSecretVersionsCache, config) c.updateStatus(ctx, upstream, conditions.Conditions()) diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go index cbfb51299..3801cb21e 100644 --- a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -30,7 +30,7 @@ const ( ErrNoCertificates = constable.Error("no certificates found") LDAPBindAccountSecretType = corev1.SecretTypeBasicAuth - TestLDAPConnectionTimeout = 90 * time.Second + probeLDAPTimeout = 90 * time.Second // Constants related to conditions. typeBindSecretValid = "BindSecretValid" @@ -290,7 +290,7 @@ func ValidateGenericLDAP(ctx context.Context, upstream UpstreamGenericLDAPIDP, s var ldapConnectionValidCondition *v1alpha1.Condition var searchBaseFoundCondition *v1alpha1.Condition if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { - ldapConnectionValidCondition, searchBaseFoundCondition = validateAndSetLDAPServerConnectivity(ctx, validatedSecretVersionsCache, upstream, config, currentSecretVersion) + ldapConnectionValidCondition, searchBaseFoundCondition = validateAndSetLDAPServerConnectivityAndSearchBase(ctx, validatedSecretVersionsCache, upstream, config, currentSecretVersion) if ldapConnectionValidCondition != nil { conditions.Append(ldapConnectionValidCondition, false) } @@ -301,10 +301,10 @@ func ValidateGenericLDAP(ctx context.Context, upstream UpstreamGenericLDAPIDP, s return conditions } -func validateAndSetLDAPServerConnectivity(ctx context.Context, validatedSecretVersionsCache *SecretVersionCache, upstream UpstreamGenericLDAPIDP, config *upstreamldap.ProviderConfig, currentSecretVersion string) (*v1alpha1.Condition, *v1alpha1.Condition) { +func validateAndSetLDAPServerConnectivityAndSearchBase(ctx context.Context, validatedSecretVersionsCache *SecretVersionCache, upstream UpstreamGenericLDAPIDP, config *upstreamldap.ProviderConfig, currentSecretVersion string) (*v1alpha1.Condition, *v1alpha1.Condition) { var ldapConnectionValidCondition *v1alpha1.Condition if !HasPreviousSuccessfulTLSConnectionConditionForCurrentSpecGenerationAndSecretVersion(validatedSecretVersionsCache, upstream.Generation(), upstream.Status().Conditions(), upstream.Name(), currentSecretVersion, config) { - testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, TestLDAPConnectionTimeout) + testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, probeLDAPTimeout) defer cancelFunc() ldapConnectionValidCondition = TestConnection(testConnectionTimeout, upstream.Spec().BindSecretName(), config, currentSecretVersion) @@ -321,7 +321,11 @@ func validateAndSetLDAPServerConnectivity(ctx context.Context, validatedSecretVe } var searchBaseFoundCondition *v1alpha1.Condition if !HasPreviousSuccessfulSearchBaseConditionForCurrentGeneration(validatedSecretVersionsCache, upstream.Generation(), upstream.Status().Conditions(), upstream.Name(), currentSecretVersion, config) { - searchBaseFoundCondition = upstream.Spec().DetectAndSetSearchBase(ctx, config) + searchBaseTimeout, cancelFunc := context.WithTimeout(ctx, probeLDAPTimeout) + defer cancelFunc() + + searchBaseFoundCondition = upstream.Spec().DetectAndSetSearchBase(searchBaseTimeout, config) + validatedSettings := validatedSecretVersionsCache.ValidatedSettingsByName[upstream.Name()] validatedSettings.GroupSearchBase = config.GroupSearch.Base validatedSettings.UserSearchBase = config.UserSearch.Base diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 61fcb1181..808643362 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -660,6 +660,139 @@ func TestE2EFullIntegration(t *testing.T) { expectedGroups, ) }) + + // Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands + // by interacting with the CLI's username and password prompts. + t.Run("with Supervisor ActiveDirectory upstream IDP using username and password prompts", func(t *testing.T) { + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("Active Directory integration test requires connectivity to an LDAP server") + } + if env.SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } + + expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue + expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames + + setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/ad-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.SupervisorUpstreamActiveDirectory.TestUserPassword + "\n") + require.NoError(t, err) + + // Read all output from the subprocess until EOF. + // Ignore any errors returned because there is always an error on linux. + kubectlOutputBytes, _ := ioutil.ReadAll(ptyFile) + requireKubectlGetNamespaceOutput(t, env, string(kubectlOutputBytes)) + + t.Logf("first kubectl command took %s", time.Since(start).String()) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env, + downstream, + kubeconfigPath, + sessionCachePath, + pinnipedExe, + expectedUsername, + expectedGroups, + ) + }) + + // Add an ActiveDirectory upstream IDP and try using it to authenticate during kubectl commands + // by passing username and password via environment variables, thus avoiding the CLI's username and password prompts. + t.Run("with Supervisor ActiveDirectory upstream IDP using PINNIPED_USERNAME and PINNIPED_PASSWORD env vars", func(t *testing.T) { + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("ActiveDirectory integration test requires connectivity to an LDAP server") + } + + if env.SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } + + expectedUsername := env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue + expectedGroups := env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames + + setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env) + + // Use a specific session cache for this test. + sessionCachePath := tempDir + "/ad-test-with-env-vars-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, + }) + + // Set up the username and password env vars to avoid the interactive prompts. + const usernameEnvVar = "PINNIPED_USERNAME" + originalUsername, hadOriginalUsername := os.LookupEnv(usernameEnvVar) + t.Cleanup(func() { + if hadOriginalUsername { + require.NoError(t, os.Setenv(usernameEnvVar, originalUsername)) + } + }) + require.NoError(t, os.Setenv(usernameEnvVar, expectedUsername)) + const passwordEnvVar = "PINNIPED_PASSWORD" //nolint:gosec // this is not a credential + originalPassword, hadOriginalPassword := os.LookupEnv(passwordEnvVar) + t.Cleanup(func() { + if hadOriginalPassword { + require.NoError(t, os.Setenv(passwordEnvVar, originalPassword)) + } + }) + require.NoError(t, os.Setenv(passwordEnvVar, env.SupervisorUpstreamActiveDirectory.TestUserPassword)) + + // Run "kubectl get namespaces" which should run an LDAP-style login without interactive prompts for username and password. + 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) + + // Read all output from the subprocess until EOF. + // Ignore any errors returned because there is always an error on linux. + kubectlOutputBytes, _ := ioutil.ReadAll(ptyFile) + requireKubectlGetNamespaceOutput(t, env, string(kubectlOutputBytes)) + + t.Logf("first kubectl command took %s", time.Since(start).String()) + + // The next kubectl command should not require auth, so we should be able to run it without these env vars. + require.NoError(t, os.Unsetenv(usernameEnvVar)) + require.NoError(t, os.Unsetenv(passwordEnvVar)) + + requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env, + downstream, + kubeconfigPath, + sessionCachePath, + pinnipedExe, + expectedUsername, + expectedGroups, + ) + }) } func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib.TestEnv) { @@ -710,6 +843,39 @@ func setupClusterForEndToEndLDAPTest(t *testing.T, username string, env *testlib }, idpv1alpha1.LDAPPhaseReady) } +func setupClusterForEndToEndActiveDirectoryTest(t *testing.T, username string, env *testlib.TestEnv) { + // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. + testlib.CreateTestClusterRoleBinding(t, + rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: username}, + rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"}, + ) + testlib.WaitForUserToHaveAccess(t, username, []string{}, &authorizationv1.ResourceAttributes{ + Verb: "get", + Group: "", + Version: "v1", + Resource: "namespaces", + }) + + // Put the bind service account's info into a Secret. + bindSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", corev1.SecretTypeBasicAuth, + map[string]string{ + corev1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, + corev1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, + }, + ) + + // Create upstream LDAP provider and wait for it to become ready. + testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ + Host: env.SupervisorUpstreamActiveDirectory.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), + }, + Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ + SecretName: bindSecret.Name, + }, + }, idpv1alpha1.ActiveDirectoryPhaseReady) +} + func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) string { readFromFile := "" diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index e4165c5cb..504f4fef5 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -69,9 +69,9 @@ func TestSupervisorLogin(t *testing.T) { }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", // the ID token Username should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", }, { name: "oidc with custom username and groups claim settings", @@ -98,8 +98,8 @@ func TestSupervisorLogin(t *testing.T) { }, idpv1alpha1.PhaseReady) }, requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlow, - wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username), + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username) + "$", wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, { @@ -132,9 +132,9 @@ func TestSupervisorLogin(t *testing.T) { ) }, // the ID token Subject should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", // the ID token Username should include the upstream user ID after the upstream issuer name - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", }, { name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS", @@ -193,13 +193,13 @@ func TestSupervisorLogin(t *testing.T) { ) }, // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( - "ldaps://" + env.SupervisorUpstreamLDAP.Host + - "?base=" + url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase) + - "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ), + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamLDAP.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ + "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), + ) + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), + wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue) + "$", wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { @@ -259,13 +259,13 @@ func TestSupervisorLogin(t *testing.T) { ) }, // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( - "ldaps://" + env.SupervisorUpstreamLDAP.StartTLSOnlyHost + - "?base=" + url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase) + - "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), - ), + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamLDAP.StartTLSOnlyHost+ + "?base="+url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase)+ + "&sub="+base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), + ) + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN), + wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN) + "$", wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs, }, { @@ -372,13 +372,13 @@ func TestSupervisorLogin(t *testing.T) { ) }, // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( - "ldaps://" + env.SupervisorUpstreamActiveDirectory.Host + - "?base=" + url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase) + - "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ), + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase)+ + "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, + ) + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue), + wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserPrincipalNameValue) + "$", wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountPlusDomainNames, }, { name: "activedirectory with custom options", @@ -439,13 +439,13 @@ func TestSupervisorLogin(t *testing.T) { ) }, // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute - wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( - "ldaps://" + env.SupervisorUpstreamActiveDirectory.Host + - "?base=" + url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase) + - "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, - ), + wantDownstreamIDTokenSubjectToMatch: "^" + regexp.QuoteMeta( + "ldaps://"+env.SupervisorUpstreamActiveDirectory.Host+ + "?base="+url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase)+ + "&sub="+env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, + ) + "$", // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute - wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue), + wantDownstreamIDTokenUsernameToMatch: "^" + regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue) + "$", wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs, }, { diff --git a/test/testlib/env.go b/test/testlib/env.go index 9caf9be53..22085fbdb 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -283,7 +283,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), - TestUserPrincipalNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserPrincipalNameValue: wantEnv("PINNIPED_TEST_AD_USER_USER_PRINCIPAL_NAME", ""), TestUserMailAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_VALUE", ""), TestUserMailAttributeName: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_NAME", ""), TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), From 43694777d508ea64c244911a085549a3ee53b557 Mon Sep 17 00:00:00 2001 From: Margo Crawford Date: Thu, 26 Aug 2021 16:55:43 -0700 Subject: [PATCH 36/36] Change some comments on API docs, fix lint error by ignoring it --- ...es_activedirectoryidentityprovider.go.tmpl | 19 ++++++++++++++--- ....dev_activedirectoryidentityproviders.yaml | 21 ++++++++++++++++--- generated/1.17/README.adoc | 8 +++---- .../types_activedirectoryidentityprovider.go | 19 ++++++++++++++--- ....dev_activedirectoryidentityproviders.yaml | 21 ++++++++++++++++--- generated/1.18/README.adoc | 8 +++---- .../types_activedirectoryidentityprovider.go | 19 ++++++++++++++--- ....dev_activedirectoryidentityproviders.yaml | 21 ++++++++++++++++--- generated/1.19/README.adoc | 8 +++---- .../types_activedirectoryidentityprovider.go | 19 ++++++++++++++--- ....dev_activedirectoryidentityproviders.yaml | 21 ++++++++++++++++--- generated/1.20/README.adoc | 8 +++---- .../types_activedirectoryidentityprovider.go | 19 ++++++++++++++--- ....dev_activedirectoryidentityproviders.yaml | 21 ++++++++++++++++--- .../types_activedirectoryidentityprovider.go | 19 ++++++++++++++--- test/integration/e2e_test.go | 2 +- 16 files changed, 203 insertions(+), 50 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl index 5889bef6b..d4e481b94 100644 --- a/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go.tmpl @@ -64,7 +64,8 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", + // where domain is constructed from the domain components of the group DN. // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,7 +73,11 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for users. + // It may make sense to specify a subtree as a search base if you wish to exclude some users + // or to make searches faster. // +optional Base string `json:"base,omitempty"` @@ -83,6 +88,10 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + // This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, + // and is not shown in advanced view only + // (which would likely mean its a system created service account with advanced permissions). + // Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. // +optional Filter string `json:"filter,omitempty"` @@ -95,7 +104,11 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. // "ou=groups,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for groups. + // It may make sense to specify a subtree as a search base if you wish to exclude some groups + // for security reasons or to make searches faster. // +optional Base string `json:"base,omitempty"` diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 87f37a021..9929b2fc7 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,14 +89,20 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to a custom field that looks like "sAMAccountName@domain". + to a custom field that looks like "sAMAccountName@domain", + where domain is constructed from the domain components of + the group DN. type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for groups. + It may make sense to specify a subtree as a search base if you + wish to exclude some groups for security reasons or to make + searches faster. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -155,7 +161,10 @@ spec: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for users. + It may make sense to specify a subtree as a search base if you + wish to exclude some users or to make searches faster. type: string filter: description: Filter is the search filter which should be applied @@ -167,6 +176,12 @@ spec: dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + This means that the user is a person, is not a computer, the + sAMAccountType is for a normal user account, and is not shown + in advanced view only (which would likely mean its a system + created service account with advanced permissions). Also, either + the sAMAccountName, the userPrincipalName, or the mail attribute + matches the input username. type: string type: object required: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 6c7b41294..479b7026c 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -798,7 +798,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for groups. It may make sense to specify a subtree as a search base if you wish to exclude some groups for security reasons or to make searches faster. | *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", where domain is constructed from the domain components of the group DN. |=== @@ -875,8 +875,8 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for users. It may make sense to specify a subtree as a search base if you wish to exclude some users or to make searches faster. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, and is not shown in advanced view only (which would likely mean its a system created service account with advanced permissions). Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 5889bef6b..d4e481b94 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -64,7 +64,8 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", + // where domain is constructed from the domain components of the group DN. // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,7 +73,11 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for users. + // It may make sense to specify a subtree as a search base if you wish to exclude some users + // or to make searches faster. // +optional Base string `json:"base,omitempty"` @@ -83,6 +88,10 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + // This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, + // and is not shown in advanced view only + // (which would likely mean its a system created service account with advanced permissions). + // Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. // +optional Filter string `json:"filter,omitempty"` @@ -95,7 +104,11 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. // "ou=groups,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for groups. + // It may make sense to specify a subtree as a search base if you wish to exclude some groups + // for security reasons or to make searches faster. // +optional Base string `json:"base,omitempty"` diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 87f37a021..9929b2fc7 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,14 +89,20 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to a custom field that looks like "sAMAccountName@domain". + to a custom field that looks like "sAMAccountName@domain", + where domain is constructed from the domain components of + the group DN. type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for groups. + It may make sense to specify a subtree as a search base if you + wish to exclude some groups for security reasons or to make + searches faster. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -155,7 +161,10 @@ spec: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for users. + It may make sense to specify a subtree as a search base if you + wish to exclude some users or to make searches faster. type: string filter: description: Filter is the search filter which should be applied @@ -167,6 +176,12 @@ spec: dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + This means that the user is a person, is not a computer, the + sAMAccountType is for a normal user account, and is not shown + in advanced view only (which would likely mean its a system + created service account with advanced permissions). Also, either + the sAMAccountName, the userPrincipalName, or the mail attribute + matches the input username. type: string type: object required: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index bb624cff2..1e0f5b169 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -798,7 +798,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for groups. It may make sense to specify a subtree as a search base if you wish to exclude some groups for security reasons or to make searches faster. | *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", where domain is constructed from the domain components of the group DN. |=== @@ -875,8 +875,8 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for users. It may make sense to specify a subtree as a search base if you wish to exclude some users or to make searches faster. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, and is not shown in advanced view only (which would likely mean its a system created service account with advanced permissions). Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 5889bef6b..d4e481b94 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -64,7 +64,8 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", + // where domain is constructed from the domain components of the group DN. // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,7 +73,11 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for users. + // It may make sense to specify a subtree as a search base if you wish to exclude some users + // or to make searches faster. // +optional Base string `json:"base,omitempty"` @@ -83,6 +88,10 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + // This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, + // and is not shown in advanced view only + // (which would likely mean its a system created service account with advanced permissions). + // Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. // +optional Filter string `json:"filter,omitempty"` @@ -95,7 +104,11 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. // "ou=groups,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for groups. + // It may make sense to specify a subtree as a search base if you wish to exclude some groups + // for security reasons or to make searches faster. // +optional Base string `json:"base,omitempty"` diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 87f37a021..9929b2fc7 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,14 +89,20 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to a custom field that looks like "sAMAccountName@domain". + to a custom field that looks like "sAMAccountName@domain", + where domain is constructed from the domain components of + the group DN. type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for groups. + It may make sense to specify a subtree as a search base if you + wish to exclude some groups for security reasons or to make + searches faster. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -155,7 +161,10 @@ spec: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for users. + It may make sense to specify a subtree as a search base if you + wish to exclude some users or to make searches faster. type: string filter: description: Filter is the search filter which should be applied @@ -167,6 +176,12 @@ spec: dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + This means that the user is a person, is not a computer, the + sAMAccountType is for a normal user account, and is not shown + in advanced view only (which would likely mean its a system + created service account with advanced permissions). Also, either + the sAMAccountName, the userPrincipalName, or the mail attribute + matches the input username. type: string type: object required: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 9aee0bb4d..f1bae9ef7 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -798,7 +798,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for groups. It may make sense to specify a subtree as a search base if you wish to exclude some groups for security reasons or to make searches faster. | *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", where domain is constructed from the domain components of the group DN. |=== @@ -875,8 +875,8 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for users. It may make sense to specify a subtree as a search base if you wish to exclude some users or to make searches faster. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, and is not shown in advanced view only (which would likely mean its a system created service account with advanced permissions). Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 5889bef6b..d4e481b94 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -64,7 +64,8 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", + // where domain is constructed from the domain components of the group DN. // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,7 +73,11 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for users. + // It may make sense to specify a subtree as a search base if you wish to exclude some users + // or to make searches faster. // +optional Base string `json:"base,omitempty"` @@ -83,6 +88,10 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + // This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, + // and is not shown in advanced view only + // (which would likely mean its a system created service account with advanced permissions). + // Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. // +optional Filter string `json:"filter,omitempty"` @@ -95,7 +104,11 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. // "ou=groups,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for groups. + // It may make sense to specify a subtree as a search base if you wish to exclude some groups + // for security reasons or to make searches faster. // +optional Base string `json:"base,omitempty"` diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 87f37a021..9929b2fc7 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,14 +89,20 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to a custom field that looks like "sAMAccountName@domain". + to a custom field that looks like "sAMAccountName@domain", + where domain is constructed from the domain components of + the group DN. type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for groups. + It may make sense to specify a subtree as a search base if you + wish to exclude some groups for security reasons or to make + searches faster. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -155,7 +161,10 @@ spec: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for users. + It may make sense to specify a subtree as a search base if you + wish to exclude some users or to make searches faster. type: string filter: description: Filter is the search filter which should be applied @@ -167,6 +176,12 @@ spec: dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + This means that the user is a person, is not a computer, the + sAMAccountType is for a normal user account, and is not shown + in advanced view only (which would likely mean its a system + created service account with advanced permissions). Also, either + the sAMAccountName, the userPrincipalName, or the mail attribute + matches the input username. type: string type: object required: diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 7d4e645b7..efcde7abb 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -798,7 +798,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for groups. It may make sense to specify a subtree as a search base if you wish to exclude some groups for security reasons or to make searches faster. | *`filter`* __string__ | Filter is the ActiveDirectory search filter which should be applied when searching for groups for a user. The pattern "{}" must occur in the filter at least once and will be dynamically replaced by the dn (distinguished name) of the user entry found as a result of the user search. E.g. "member={}" or "&(objectClass=groupOfNames)(member={})". For more information about ActiveDirectory 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 "(&(objectClass=group)(member:1.2.840.113556.1.4.1941:={})". This searches nested groups by default. Note that nested group search can be slow for some Active Directory servers. To disable it, you can set the filter to "(&(objectClass=group)(member={})" | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityprovidergroupsearchattributes[$$ActiveDirectoryIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each ActiveDirectory entry which was found as the result of the group search. |=== @@ -817,7 +817,7 @@ ActiveDirectoryIdentityProvider describes the configuration of an upstream Micro [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the Active Directory entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", where domain is constructed from the domain components of the group DN. |=== @@ -875,8 +875,8 @@ Status of an Active Directory identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the default naming context. -| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' +| *`base`* __string__ | Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). The default behavior searches your entire domain for users. It may make sense to specify a subtree as a search base if you wish to exclude some users or to make searches faster. +| *`filter`* __string__ | Filter is the search filter which should be applied when searching for users. The pattern "{}" must occur in the filter at least once 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 be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, and is not shown in advanced view only (which would likely mean its a system created service account with advanced permissions). Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-activedirectoryidentityproviderusersearchattributes[$$ActiveDirectoryIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the ActiveDirectory entry which was found as the result of the user search. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 5889bef6b..d4e481b94 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -64,7 +64,8 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", + // where domain is constructed from the domain components of the group DN. // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,7 +73,11 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for users. + // It may make sense to specify a subtree as a search base if you wish to exclude some users + // or to make searches faster. // +optional Base string `json:"base,omitempty"` @@ -83,6 +88,10 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + // This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, + // and is not shown in advanced view only + // (which would likely mean its a system created service account with advanced permissions). + // Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. // +optional Filter string `json:"filter,omitempty"` @@ -95,7 +104,11 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. // "ou=groups,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for groups. + // It may make sense to specify a subtree as a search base if you wish to exclude some groups + // for security reasons or to make searches faster. // +optional Base string `json:"base,omitempty"` diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml index 87f37a021..9929b2fc7 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_activedirectoryidentityproviders.yaml @@ -89,14 +89,20 @@ spec: the ActiveDirectory server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, this defaults - to a custom field that looks like "sAMAccountName@domain". + to a custom field that looks like "sAMAccountName@domain", + where domain is constructed from the domain components of + the group DN. type: string type: object base: description: Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. "ou=groups,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for groups. + It may make sense to specify a subtree as a search base if you + wish to exclude some groups for security reasons or to make + searches faster. type: string filter: description: Filter is the ActiveDirectory search filter which @@ -155,7 +161,10 @@ spec: description: Base is the dn (distinguished name) that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". Optional, when not specified it will be based on the result - of a query for the default naming context. + of a query for the defaultNamingContext (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + The default behavior searches your entire domain for users. + It may make sense to specify a subtree as a search base if you + wish to exclude some users or to make searches faster. type: string filter: description: Filter is the search filter which should be applied @@ -167,6 +176,12 @@ spec: dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will be '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + This means that the user is a person, is not a computer, the + sAMAccountType is for a normal user account, and is not shown + in advanced view only (which would likely mean its a system + created service account with advanced permissions). Also, either + the sAMAccountName, the userPrincipalName, or the mail attribute + matches the input username. type: string type: object required: diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go index 5889bef6b..d4e481b94 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_activedirectoryidentityprovider.go @@ -64,7 +64,8 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the ActiveDirectory // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain". + // Optional. When not specified, this defaults to a custom field that looks like "sAMAccountName@domain", + // where domain is constructed from the domain components of the group DN. // +optional GroupName string `json:"groupName,omitempty"` } @@ -72,7 +73,11 @@ type ActiveDirectoryIdentityProviderGroupSearchAttributes struct { type ActiveDirectoryIdentityProviderUserSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for users. // E.g. "ou=users,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for users. + // It may make sense to specify a subtree as a search base if you wish to exclude some users + // or to make searches faster. // +optional Base string `json:"base,omitempty"` @@ -83,6 +88,10 @@ type ActiveDirectoryIdentityProviderUserSearch struct { // 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 be // '(&(objectClass=person)(!(objectClass=computer))(!(showInAdvancedViewOnly=TRUE))(|(sAMAccountName={}")(mail={})(userPrincipalName={})(sAMAccountType=805306368))' + // This means that the user is a person, is not a computer, the sAMAccountType is for a normal user account, + // and is not shown in advanced view only + // (which would likely mean its a system created service account with advanced permissions). + // Also, either the sAMAccountName, the userPrincipalName, or the mail attribute matches the input username. // +optional Filter string `json:"filter,omitempty"` @@ -95,7 +104,11 @@ type ActiveDirectoryIdentityProviderUserSearch struct { type ActiveDirectoryIdentityProviderGroupSearch struct { // Base is the dn (distinguished name) that should be used as the search base when searching for groups. E.g. // "ou=groups,dc=example,dc=com". - // Optional, when not specified it will be based on the result of a query for the default naming context. + // Optional, when not specified it will be based on the result of a query for the defaultNamingContext + // (see https://docs.microsoft.com/en-us/windows/win32/adschema/rootdse). + // The default behavior searches your entire domain for groups. + // It may make sense to specify a subtree as a search base if you wish to exclude some groups + // for security reasons or to make searches faster. // +optional Base string `json:"base,omitempty"` diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 808643362..f152b54a7 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -47,7 +47,7 @@ import ( ) // TestE2EFullIntegration tests a full integration scenario that combines the supervisor, concierge, and CLI. -func TestE2EFullIntegration(t *testing.T) { +func TestE2EFullIntegration(t *testing.T) { // nolint:gocyclo env := testlib.IntegrationEnv(t) ctx, cancelFunc := context.WithTimeout(context.Background(), 15*time.Minute)