diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index 1550f0c7..0861c0de 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 485b3f96..8de20adc 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -808,8 +808,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -827,7 +827,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index 57bfe7d2..6de19496 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -808,8 +808,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -827,7 +827,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index db1c1590..37d2251c 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -808,8 +808,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -827,7 +827,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 25cfd6ff..a24ce0b5 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -808,8 +808,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter. E.g. "mail" or "uid" or "userPrincipalName". -| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". +| *`username`* __string__ | Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`uniqueID`* __string__ | UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== @@ -827,7 +827,7 @@ Status of an LDAP identity provider. |=== | Field | Description | *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". +| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. Optional. When not specified, the default will act as if the Filter were specified as the value from Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be explicitly specified, since the default value of "dn={}" would not work. | *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearchattributesspec[$$LDAPIdentityProviderUserSearchAttributesSpec$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index db84c861..d262f36f 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -97,15 +97,26 @@ spec: description: UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful - authentication. E.g. "uidNumber" or "objectGUID". + authentication. E.g. "uidNumber" or "objectGUID". The value + of this field is case-sensitive and must match the case + of the attribute name returned by the LDAP server in the + user's entry. Distinguished names can be used by specifying + lower-case "dn". minLength: 1 type: string username: description: Username specifies the name of attribute in the LDAP entry which whose value shall become the username of the user after a successful authentication. This would typically - be the same attribute name used in the user search filter. - E.g. "mail" or "uid" or "userPrincipalName". + be the same attribute name used in the user search filter, + although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + The value of this field is case-sensitive and must match + the case of the attribute name returned by the LDAP server + in the user's entry. Distinguished names can be used by + specifying lower-case "dn". When this field is set to "dn" + then the LDAPIdentityProviderUserSearchSpec's Filter field + cannot be blank, since the default value of "dn={}" would + not work. minLength: 1 type: string type: object @@ -120,9 +131,12 @@ spec: in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. - Optional. When not specified, the default will act as if the - Filter were specified as the value from Attributes.Username - appended by "={}". + Note that the dn (distinguished name) is not an attribute of + an entry, so "dn={}" cannot be used. Optional. When not specified, + the default will act as if the Filter were specified as the + value from Attributes.Username appended by "={}". When the Attributes.Username + is set to "dn" then the Filter must be explicitly specified, + since the default value of "dn={}" would not work. type: string type: object required: diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 1550f0c7..0861c0de 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -54,12 +54,18 @@ type LDAPIdentityProviderBindSpec struct { type LDAPIdentityProviderUserSearchAttributesSpec struct { // Username specifies the name of attribute in the LDAP entry which whose value shall become the username // of the user after a successful authentication. This would typically be the same attribute name used in - // the user search filter. E.g. "mail" or "uid" or "userPrincipalName". + // the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field + // is set to "dn" then the LDAPIdentityProviderUserSearchSpec's Filter field cannot be blank, since the default + // value of "dn={}" would not work. // +kubebuilder:validation:MinLength=1 Username string `json:"username,omitempty"` // UniqueID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely // identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". + // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP + // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". // +kubebuilder:validation:MinLength=1 UniqueID string `json:"uniqueID,omitempty"` } @@ -72,8 +78,10 @@ type LDAPIdentityProviderUserSearchSpec struct { // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // Note that the dn (distinguished name) is not an attribute of an entry, so "dn={}" cannot be used. // Optional. When not specified, the default will act as if the Filter were specified as the value from - // Attributes.Username appended by "={}". + // Attributes.Username appended by "={}". When the Attributes.Username is set to "dn" then the Filter must be + // explicitly specified, since the default value of "dn={}" would not work. // +optional Filter string `json:"filter,omitempty"` diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index b5d79c80..a5af4fe6 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -180,6 +180,11 @@ func (p *Provider) TestAuthenticateUser(ctx context.Context, testUsername string // Authenticate a user and return their mapped username, groups, and UID. Implements authenticators.UserAuthenticator. func (p *Provider) AuthenticateUser(ctx context.Context, username, password string) (*authenticator.Response, bool, error) { + if p.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.UserSearch.Filter) == 0 { + // LDAP search filters do not allow searching by DN. + return nil, false, fmt.Errorf(`must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`) + } + conn, err := p.dial(ctx) if err != nil { return nil, false, fmt.Errorf(`error dialing host "%s": %w`, p.Host, err) @@ -269,9 +274,13 @@ func (p *Provider) userSearchRequestedAttributes() []string { func (p *Provider) userSearchFilter(username string) string { safeUsername := p.escapeUsernameForSearchFilter(username) if len(p.UserSearch.Filter) == 0 { - return fmt.Sprintf("%s=%s", p.UserSearch.UsernameAttribute, safeUsername) + return fmt.Sprintf("(%s=%s)", p.UserSearch.UsernameAttribute, safeUsername) } - return strings.ReplaceAll(p.UserSearch.Filter, userSearchFilterInterpolationLocationMarker, safeUsername) + filter := strings.ReplaceAll(p.UserSearch.Filter, userSearchFilterInterpolationLocationMarker, safeUsername) + if strings.HasPrefix(filter, "(") && strings.HasSuffix(filter, ")") { + return filter + } + return "(" + filter + ")" } func (p *Provider) escapeUsernameForSearchFilter(username string) string { diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 61b2a305..39b19093 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -25,7 +25,7 @@ import ( const ( testHost = "ldap.example.com:8443" - testBindUsername = "some-bind-username" + testBindUsername = "cn=some-bind-username,dc=pinniped,dc=dev" testBindPassword = "some-bind-password" testUpstreamUsername = "some-upstream-username" testUpstreamPassword = "some-upstream-password" @@ -39,7 +39,7 @@ const ( ) var ( - testUserSearchFilterInterpolated = fmt.Sprintf("some-filter=%s-and-more-filter=%s", testUpstreamUsername, testUpstreamUsername) + testUserSearchFilterInterpolated = fmt.Sprintf("(some-filter=%s-and-more-filter=%s)", testUpstreamUsername, testUpstreamUsername) ) func TestAuthenticateUser(t *testing.T) { @@ -87,6 +87,7 @@ func TestAuthenticateUser(t *testing.T) { setupMocks func(conn *mockldapconn.MockConn) dialError error wantError string + wantToSkipDial bool wantAuthResponse *authenticator.Response }{ { @@ -115,13 +116,44 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultUIDAttributeValue, + Groups: []string{}, }, }, }, { - name: "when the UsernameAttribute is dn", + name: "when the user search filter is already wrapped by parenthesis then it is not wrapped again", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(func(p *Provider) { + p.UserSearch.Filter = "(" + testUserSearchFilter + ")" + }), + setupMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: testSearchResultDNValue, + Attributes: []*ldap.EntryAttribute{ + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + }, + }, + }, + }, nil).Times(1) + conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Times(1) + conn.EXPECT().Close().Times(1) + }, + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{ + Name: testSearchResultUsernameAttributeValue, + UID: testSearchResultUIDAttributeValue, + Groups: []string{}, + }, + }, + }, + { + name: "when the UsernameAttribute is dn and there is a user search filter provided", username: testUpstreamUsername, password: testUpstreamPassword, provider: provider(func(p *Provider) { @@ -147,8 +179,8 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultDNValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultUIDAttributeValue, + Groups: []string{}, }, }, }, @@ -179,8 +211,8 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultDNValue, + Groups: []string{}, }, }, }, @@ -194,7 +226,7 @@ func TestAuthenticateUser(t *testing.T) { setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { - r.Filter = testUserSearchUsernameAttribute + "=" + testUpstreamUsername + r.Filter = "(" + testUserSearchUsernameAttribute + "=" + testUpstreamUsername + ")" })).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -212,8 +244,8 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultUIDAttributeValue, + Groups: []string{}, }, }, }, @@ -225,7 +257,7 @@ func TestAuthenticateUser(t *testing.T) { setupMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { - r.Filter = fmt.Sprintf("some-filter=%s-and-more-filter=%s", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) + r.Filter = fmt.Sprintf("(some-filter=%s-and-more-filter=%s)", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) })).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -243,18 +275,28 @@ func TestAuthenticateUser(t *testing.T) { wantAuthResponse: &authenticator.Response{ User: &user.DefaultInfo{ Name: testSearchResultUsernameAttributeValue, - Groups: []string{}, // We don't support group search yet. Coming soon! UID: testSearchResultUIDAttributeValue, + Groups: []string{}, }, }, }, - // TODO are LDAP attribute names case sensitive? do we need any special handling for case? { name: "when dial fails", provider: provider(nil), dialError: errors.New("some dial error"), wantError: fmt.Sprintf(`error dialing host "%s": some dial error`, testHost), }, + { + name: "when the UsernameAttribute is dn and there is not a user search filter provided", + username: testUpstreamUsername, + password: testUpstreamPassword, + provider: provider(func(p *Provider) { + p.UserSearch.UsernameAttribute = "dn" + p.UserSearch.Filter = "" + }), + wantToSkipDial: true, + wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`, + }, { name: "when binding as the bind user returns an error", provider: provider(nil), @@ -507,7 +549,7 @@ func TestAuthenticateUser(t *testing.T) { authResponse, authenticated, err := tt.provider.AuthenticateUser(context.Background(), tt.username, tt.password) - require.True(t, dialWasAttempted, "AuthenticateUser was supposed to try to dial, but didn't") + require.Equal(t, !tt.wantToSkipDial, dialWasAttempted) if tt.wantError != "" { require.EqualError(t, err, tt.wantError) diff --git a/test/integration/ldapsearch_test.go b/test/integration/ldapsearch_test.go new file mode 100644 index 00000000..6bd36241 --- /dev/null +++ b/test/integration/ldapsearch_test.go @@ -0,0 +1,608 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package integration + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net" + "os" + "os/exec" + "path" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" + + "go.pinniped.dev/internal/certauthority" + "go.pinniped.dev/internal/upstreamldap" +) + +func TestLDAPSearch(t *testing.T) { + ctx, cancelFunc := context.WithCancel(context.Background()) + t.Cleanup(func() { + cancelFunc() // this will send SIGKILL to the docker process, just in case + }) + + port := localhostPort(t) + caBundle := dockerRunLDAPServer(ctx, t, port) + + provider := func(editFunc func(p *upstreamldap.Provider)) *upstreamldap.Provider { + provider := &upstreamldap.Provider{ + Name: "test-ldap-provider", + Host: "127.0.0.1:" + port, + CABundle: caBundle, + BindUsername: "cn=admin,dc=pinniped,dc=dev", + BindPassword: "password", + UserSearch: &upstreamldap.UserSearch{ + Base: "ou=users,dc=pinniped,dc=dev", + Filter: "", // defaults to UsernameAttribute={}, i.e. "cn={}" in this case + UsernameAttribute: "cn", + UIDAttribute: "uidNumber", + }, + } + if editFunc != nil { + editFunc(provider) + } + return provider + } + + pinnyPassword := "password123" // from the LDIF file below + wallyPassword := "password456" // from the LDIF file below + + tests := []struct { + name string + username string + password string + provider *upstreamldap.Provider + wantError string + wantAuthResponse *authenticator.Response + }{ + { + name: "happy path", + username: "pinny", + password: pinnyPassword, + provider: provider(nil), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "happy path as a different user", + username: "wally", + password: wallyPassword, + provider: provider(nil), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "wally", UID: "1001", Groups: []string{}}, + }, + }, + { + name: "using a different user search base", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "dc=pinniped,dc=dev" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the user search filter is already wrapped by parenthesis", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "(cn={})" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the UsernameAttribute is dn and a user search filter is provided", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.UsernameAttribute = "dn" + p.UserSearch.Filter = "cn={}" + }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the user search filter allows for different ways of logging in and the first one is used", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.Filter = "(|(cn={})(mail={}))" + }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the user search filter allows for different ways of logging in and the second one is used", + username: "pinny.ldap@example.com", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.Filter = "(|(cn={})(mail={}))" + }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "when the UIDAttribute is dn", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "dn" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{}}, + }, + }, + { + name: "when the UIDAttribute is sn", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "sn" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{}}, + }, + }, + { + name: "when the UsernameAttribute is sn", + username: "seAl", // note that this is not case-sensitive! sn=Seal + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "sn" }), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{}}, // note that the final answer is case-sensitive + }, + }, + { + name: "when the UsernameAttribute is dn and there is no user search filter provided", + username: "cn=pinny,ou=users,dc=pinniped,dc=dev", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.UsernameAttribute = "dn" + p.UserSearch.Filter = "" + }), + wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`, + }, + { + name: "when the bind user username is not a valid DN", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.BindUsername = "invalid-dn" }), + wantError: `error binding as "invalid-dn" before user search: LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, + }, + { + name: "when the bind user username is wrong", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.BindUsername = "cn=wrong,dc=pinniped,dc=dev" }), + wantError: `error binding as "cn=wrong,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `, + }, + { + name: "when the bind user password is wrong", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.BindPassword = "wrong-password" }), + wantError: `error binding as "cn=admin,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `, + }, + { + name: "when the end user password is wrong", + username: "pinny", + password: "wrong-pinny-password", + provider: provider(nil), + wantError: `error binding for user "pinny" using provided password against DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 49 "Invalid Credentials": `, + }, + { + name: "when the end user username is wrong", + username: "wrong-username", + password: pinnyPassword, + provider: provider(nil), + wantError: `searching for user "wrong-username" resulted in 0 search results, but expected 1 result`, + }, + { + name: "when the user search filter does not compile", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Filter = "*" }), + wantError: `error searching for user "pinny": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`, + }, + { + name: "when there are too many search results for the user", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.Filter = "objectClass=*" // overly broad search filter + }), + wantError: `error searching for user "pinny": LDAP Result Code 4 "Size Limit Exceeded": `, + }, + { + name: "when the server is unreachable", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.Host = "127.0.0.1:27534" }), // hopefully this port is not in use on the host running tests + wantError: `error dialing host "127.0.0.1:27534": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:27534: connect: connection refused`, + }, + { + name: "when the server is not parsable", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.Host = "too:many:ports" }), + wantError: `error dialing host "too:many:ports": LDAP Result Code 200 "Network Error": address too:many:ports: too many colons in address`, + }, + { + name: "when the CA bundle is not parsable", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.CABundle = []byte("invalid-pem") }), + wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, port), + }, + { + name: "when the CA bundle does not cause the host to be trusted", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.CABundle = nil }), + wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, port), + }, + { + name: "when the UsernameAttribute attribute has multiple values in the entry", + username: "wally.ldap@example.com", + password: wallyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "mail" }), + wantError: `found 2 values for attribute "mail" while searching for user "wally.ldap@example.com", but expected 1 result`, + }, + { + name: "when the UIDAttribute attribute has multiple values in the entry", + username: "wally", + password: wallyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "mail" }), + wantError: `found 2 values for attribute "mail" while searching for user "wally", but expected 1 result`, + }, + { + name: "when the UsernameAttribute attribute is not found in the entry", + username: "wally", + password: wallyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.Filter = "cn={}" + p.UserSearch.UsernameAttribute = "attr-does-not-exist" + }), + wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`, + }, + { + name: "when the UIDAttribute attribute is not found in the entry", + username: "wally", + password: wallyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "attr-does-not-exist" }), + wantError: `found 0 values for attribute "attr-does-not-exist" while searching for user "wally", but expected 1 result`, + }, + { + name: "when the UsernameAttribute has the wrong case", + username: "Seal", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UsernameAttribute = "SN" }), // this is case-sensitive + wantError: `found 0 values for attribute "SN" while searching for user "Seal", but expected 1 result`, + }, + { + name: "when the UIDAttribute has the wrong case", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.UIDAttribute = "SN" }), // this is case-sensitive + wantError: `found 0 values for attribute "SN" while searching for user "pinny", but expected 1 result`, + }, + { + name: "when the UsernameAttribute is DN and has the wrong case", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.UsernameAttribute = "DN" // dn must be lower-case + p.UserSearch.Filter = "cn={}" + }), + wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`, + }, + { + name: "when the UIDAttribute is DN and has the wrong case", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { + p.UserSearch.UIDAttribute = "DN" // dn must be lower-case + }), + wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`, + }, + { + name: "when the search base is invalid", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "invalid-base" }), + wantError: `error searching for user "pinny": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, + }, + { + name: "when the search base does not exist", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" }), + wantError: `error searching for user "pinny": LDAP Result Code 32 "No Such Object": `, + }, + { + name: "when the search base causes no search results", + username: "pinny", + password: pinnyPassword, + provider: provider(func(p *upstreamldap.Provider) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" }), + wantError: `searching for user "pinny" resulted in 0 search results, but expected 1 result`, + }, + { + name: "when there is no username specified", + username: "", + password: pinnyPassword, + provider: provider(nil), + wantError: `searching for user "" resulted in 0 search results, but expected 1 result`, + }, + { + name: "when there is no password specified", + username: "pinny", + password: "", + provider: provider(nil), + wantError: `error binding for user "pinny" using provided password against DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 206 "Empty password not allowed by the client": ldap: empty password not allowed by the client`, + }, + { + name: "when the user has no password in their entry", + username: "olive", + password: "anything", + provider: provider(nil), + wantError: `error binding for user "olive" using provided password against DN "cn=olive,ou=users,dc=pinniped,dc=dev": LDAP Result Code 49 "Invalid Credentials": `, + }, + } + + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + authResponse, authenticated, err := tt.provider.AuthenticateUser(ctx, tt.username, tt.password) + + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + require.False(t, authenticated) + require.Nil(t, authResponse) + } else { + require.NoError(t, err) + require.True(t, authenticated) + require.Equal(t, tt.wantAuthResponse, authResponse) + } + }) + } +} + +func localhostPort(t *testing.T) string { + t.Helper() + + unusedPortGrabbingListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + recentlyClaimedHostAndPort := unusedPortGrabbingListener.Addr().String() + require.NoError(t, unusedPortGrabbingListener.Close()) + + splitHostAndPort := strings.Split(recentlyClaimedHostAndPort, ":") + require.Len(t, splitHostAndPort, 2) + + return splitHostAndPort[1] +} + +func dockerRunLDAPServer(ctx context.Context, t *testing.T, hostPort string) []byte { + t.Helper() + + _, err := exec.LookPath("docker") + require.NoError(t, err) + + ca, err := certauthority.New("Test LDAP CA", time.Hour*24) + require.NoError(t, err) + + certPEM, keyPEM, err := ca.IssueServerCertPEM(nil, []net.IP{net.ParseIP("127.0.0.1")}, time.Hour*24) + require.NoError(t, err) + + tempDir, err := ioutil.TempDir("", "pinniped-test-*") + require.NoError(t, err) + t.Cleanup(func() { + err := os.Remove(tempDir) + require.NoError(t, err) + }) + + writeToNewTempFile(t, tempDir, "cert.pem", certPEM) + writeToNewTempFile(t, tempDir, "key.pem", keyPEM) + writeToNewTempFile(t, tempDir, "ca.pem", ca.Bundle()) + writeToNewTempFile(t, tempDir, "test.ldif", []byte(testLDIF)) + + dockerArgs := []string{ + "run", + "-e", "BITNAMI_DEBUG=true", + "-e", "LDAP_ADMIN_USERNAME=admin", + "-e", "LDAP_ADMIN_PASSWORD=password", + "-e", "LDAP_ENABLE_TLS=yes", + "-e", "LDAP_TLS_CERT_FILE=/inputs/cert.pem", + "-e", "LDAP_TLS_KEY_FILE=/inputs/key.pem", + "-e", "LDAP_TLS_CA_FILE=/inputs/ca.pem", + "-e", "LDAP_CUSTOM_LDIF_DIR=/inputs", + "-e", "LDAP_ROOT=dc=pinniped,dc=dev", + "-v", tempDir + ":/inputs", + "-p", hostPort + ":1636", + "-m", "64m", + "--rm", // automatically delete the container when finished + "docker.io/bitnami/openldap", + } + + t.Log("Starting:", "docker", strings.Join(dockerArgs, " ")) + + cmd := exec.CommandContext(ctx, "docker", dockerArgs...) + + var stdoutBuf, stderrBuf syncBuffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) + cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + + err = cmd.Start() + require.NoError(t, err) + t.Cleanup(func() { + // docker requires an interrupt signal to end the container. + // This t.Cleanup is registered after the one that cancels the context, so this one will happen first. + err := cmd.Process.Signal(os.Interrupt) + require.NoError(t, err) + time.Sleep(time.Second) // give a moment before we move on, because we'll send SIGKILL in a later t.Cleanup + }) + + earlyTerminationCh := make(chan bool, 1) + go func() { + err = cmd.Wait() + earlyTerminationCh <- true + }() + + terminatedEarly := false + require.Eventually(t, func() bool { + t.Log("Waiting for slapd to start...") + // This substring is contained in the last line of output before the server starts. + if strings.Contains(stderrBuf.String(), " slapd starting\n") { + return true + } + select { + case <-earlyTerminationCh: + terminatedEarly = true + return true + default: // ignore when this non-blocking read found no message + } + return false + }, 2*time.Minute, time.Second) + + require.Falsef(t, terminatedEarly, "docker command ended sooner than expected") + + t.Log("Detected LDAP server has started successfully") + return ca.Bundle() +} + +func writeToNewTempFile(t *testing.T, dir string, filename string, contents []byte) { + t.Helper() + + filePath := path.Join(dir, filename) + + err := ioutil.WriteFile(filePath, contents, 0644) + require.NoError(t, err) + + t.Cleanup(func() { + err := os.Remove(filePath) + require.NoError(t, err) + }) +} + +var testLDIF = ` +# ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! *** +# Here's a good explaination of LDIF: +# https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system + +# pinniped.dev (organization, root) +dn: dc=pinniped,dc=dev +objectClass: dcObject +objectClass: organization +dc: pinniped +o: example + +# users, pinniped.dev (organization unit) +dn: ou=users,dc=pinniped,dc=dev +objectClass: organizationalUnit +ou: users + +# groups, pinniped.dev (organization unit) +dn: ou=groups,dc=pinniped,dc=dev +objectClass: organizationalUnit +ou: groups + +# beach-groups, groups, pinniped.dev (organization unit) +dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev +objectClass: organizationalUnit +ou: beach-groups + +# pinny, users, pinniped.dev (user) +dn: cn=pinny,ou=users,dc=pinniped,dc=dev +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +cn: pinny +sn: Seal +givenName: Pinny +mail: pinny.ldap@example.com +userPassword: password123 +uid: pinny +uidNumber: 1000 +gidNumber: 1000 +homeDirectory: /home/pinny +loginShell: /bin/bash +gecos: pinny-the-seal + +# wally, users, pinniped.dev +dn: cn=wally,ou=users,dc=pinniped,dc=dev +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +cn: wally +sn: Walrus +givenName: Wally +mail: wally.ldap@example.com +mail: wally.alternate@example.com +userPassword: password456 +uid: wally +uidNumber: 1001 +gidNumber: 1001 +homeDirectory: /home/wally +loginShell: /bin/bash +gecos: wally-the-walrus + +# olive, users, pinniped.dev (user without password) +dn: cn=olive,ou=users,dc=pinniped,dc=dev +objectClass: inetOrgPerson +objectClass: posixAccount +objectClass: shadowAccount +cn: olive +sn: Boston Terrier +givenName: Olive +mail: olive.ldap@example.com +uid: olive +uidNumber: 1002 +gidNumber: 1002 +homeDirectory: /home/olive +loginShell: /bin/bash +gecos: olive-the-dog + +# ball-game-players, beach-groups, groups, pinniped.dev (group of users) +dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev +cn: ball-game-players +objectClass: groupOfNames +member: cn=pinny,ou=users,dc=pinniped,dc=dev +member: cn=olive,ou=users,dc=pinniped,dc=dev + +# seals, groups, pinniped.dev (group of users) +dn: cn=seals,ou=groups,dc=pinniped,dc=dev +cn: seals +objectClass: groupOfNames +member: cn=pinny,ou=users,dc=pinniped,dc=dev + +# walruses, groups, pinniped.dev (group of users) +dn: cn=walruses,ou=groups,dc=pinniped,dc=dev +cn: walruses +objectClass: groupOfNames +member: cn=wally,ou=users,dc=pinniped,dc=dev + +# pinnipeds, users, pinniped.dev (group of groups) +dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev +cn: pinnipeds +objectClass: groupOfNames +member: cn=seals,ou=groups,dc=pinniped,dc=dev +member: cn=walruses,ou=groups,dc=pinniped,dc=dev + +# mammals, groups, pinniped.dev (group of both groups and users) +dn: cn=mammals,ou=groups,dc=pinniped,dc=dev +cn: mammals +objectClass: groupOfNames +member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev +member: cn=olive,ou=users,dc=pinniped,dc=dev +`