From 3e1e8880f7a62325cf4f2537457916aa5ff913f6 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 17 May 2021 11:10:26 -0700 Subject: [PATCH] Initial support for upstream LDAP group membership Reflect the upstream group membership into the Supervisor's downstream tokens, so they can be added to the user's identity on the workload clusters. LDAP group search is configurable on the LDAPIdentityProvider resource. --- .../types_ldapidentityprovider.go.tmpl | 49 +- ...or.pinniped.dev_ldapidentityproviders.yaml | 92 ++- generated/1.17/README.adoc | 45 +- .../v1alpha1/types_ldapidentityprovider.go | 49 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 34 + ...or.pinniped.dev_ldapidentityproviders.yaml | 92 ++- generated/1.18/README.adoc | 45 +- .../v1alpha1/types_ldapidentityprovider.go | 49 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 34 + ...or.pinniped.dev_ldapidentityproviders.yaml | 92 ++- generated/1.19/README.adoc | 45 +- .../v1alpha1/types_ldapidentityprovider.go | 49 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 34 + ...or.pinniped.dev_ldapidentityproviders.yaml | 92 ++- generated/1.20/README.adoc | 45 +- .../v1alpha1/types_ldapidentityprovider.go | 49 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 34 + ...or.pinniped.dev_ldapidentityproviders.yaml | 92 ++- .../v1alpha1/types_ldapidentityprovider.go | 49 +- .../idp/v1alpha1/zz_generated.deepcopy.go | 34 + .../ldap_upstream_watcher.go | 5 + .../ldap_upstream_watcher_test.go | 45 +- internal/mocks/mockldapconn/mockldapconn.go | 15 + internal/upstreamldap/upstreamldap.go | 132 +++- internal/upstreamldap/upstreamldap_test.go | 658 +++++++++++++----- test/integration/e2e_test.go | 43 +- test/integration/ldap_client_test.go | 135 +++- test/integration/supervisor_login_test.go | 34 +- test/library/env.go | 34 +- 29 files changed, 1783 insertions(+), 422 deletions(-) diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index d718ba65..0e28234d 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -39,14 +39,14 @@ type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // 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 LDAPIdentityProviderUserSearchAttributes struct { - // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // Username specifies the name of the attribute in the LDAP entry 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 @@ -64,14 +64,26 @@ type LDAPIdentityProviderUserSearchAttributes struct { UID string `json:"uid,omitempty"` } +type LDAPIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the LDAP 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 LDAP + // server in the user's entry. 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 "cn" (common name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + type LDAPIdentityProviderUserSearch struct { - // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // 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". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" - // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // 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 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 @@ -85,6 +97,30 @@ type LDAPIdentityProviderUserSearch struct { Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } +type LDAPIdentityProviderGroupSearch 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 LDAP provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the LDAP 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 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each LDAP entry which was found as + // the result of the group search. + // +optional + Attributes LDAPIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. @@ -100,6 +136,9 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. + GroupSearch LDAPIdentityProviderGroupSearch `json:"groupSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d396129d..46fbe1d0 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -63,13 +63,55 @@ spec: LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". - The password must be non-empty. + 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 the LDAP provider. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each LDAP entry which was found as the result + of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the LDAP 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 LDAP server + in the user's entry. 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 "cn" (common 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 LDAP provider. + Also, when not specified, the values of Filter and Attributes + are ignored. + type: string + filter: + description: Filter is the LDAP 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 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 "member={}". + type: string + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' @@ -104,37 +146,39 @@ spec: 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, - although it can be different. E.g. "mail" or "uid" or "userPrincipalName". - The value of this field is case-sensitive and must match - the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearch's Filter field cannot - be blank, since the default value of "dn={}" would not work. + description: Username specifies the name of the attribute + in the LDAP entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in the user search + filter, although it can be different. E.g. "mail" or "uid" + or "userPrincipalName". The value of this field is case-sensitive + and must match the case of the attribute name returned by + the LDAP server in the user's entry. Distinguished names + can be used by specifying lower-case "dn". When this field + is set to "dn" then the LDAPIdentityProviderUserSearch's + Filter field cannot be blank, since the default value of + "dn={}" would not work. minLength: 1 type: string type: object base: - description: Base is the DN that should be used as the search - base when searching for users. E.g. "ou=users,dc=example,dc=com". + 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". minLength: 1 type: string filter: description: Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - in the filter and will be dynamically replaced by the username - for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". - For more information about LDAP filters, see https://ldap.com/ldap-filters. - 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. + 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 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 edda79aa..b08040ec 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -734,7 +734,43 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full dn (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-ldapidentityprovidergroupsearch"] +==== LDAPIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (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 LDAP provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the LDAP 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 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 "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes[$$LDAPIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each LDAP entry which was found as the result of the group search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes"] +==== LDAPIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP 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 LDAP server in the user's entry. 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 "cn" (common name). |=== @@ -757,6 +793,7 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. |=== @@ -791,8 +828,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. 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". +| *`filter`* __string__ | Filter is the LDAP 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 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-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== @@ -810,7 +847,7 @@ 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, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`username`* __string__ | Username specifies the name of the attribute in the LDAP entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. | *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== 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 d718ba65..0e28234d 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -39,14 +39,14 @@ type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // 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 LDAPIdentityProviderUserSearchAttributes struct { - // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // Username specifies the name of the attribute in the LDAP entry 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 @@ -64,14 +64,26 @@ type LDAPIdentityProviderUserSearchAttributes struct { UID string `json:"uid,omitempty"` } +type LDAPIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the LDAP 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 LDAP + // server in the user's entry. 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 "cn" (common name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + type LDAPIdentityProviderUserSearch struct { - // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // 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". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" - // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // 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 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 @@ -85,6 +97,30 @@ type LDAPIdentityProviderUserSearch struct { Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } +type LDAPIdentityProviderGroupSearch 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 LDAP provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the LDAP 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 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each LDAP entry which was found as + // the result of the group search. + // +optional + Attributes LDAPIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. @@ -100,6 +136,9 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. + GroupSearch LDAPIdentityProviderGroupSearch `json:"groupSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index c48c570f..f7762b09 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 @@ -72,6 +72,39 @@ func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearch) DeepCopyInto(out *LDAPIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearch. +func (in *LDAPIdentityProviderGroupSearch) DeepCopy() *LDAPIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearchAttributes. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopy() *LDAPIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -115,6 +148,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) } out.Bind = in.Bind out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch return } diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d396129d..46fbe1d0 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -63,13 +63,55 @@ spec: LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". - The password must be non-empty. + 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 the LDAP provider. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each LDAP entry which was found as the result + of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the LDAP 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 LDAP server + in the user's entry. 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 "cn" (common 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 LDAP provider. + Also, when not specified, the values of Filter and Attributes + are ignored. + type: string + filter: + description: Filter is the LDAP 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 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 "member={}". + type: string + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' @@ -104,37 +146,39 @@ spec: 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, - although it can be different. E.g. "mail" or "uid" or "userPrincipalName". - The value of this field is case-sensitive and must match - the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearch's Filter field cannot - be blank, since the default value of "dn={}" would not work. + description: Username specifies the name of the attribute + in the LDAP entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in the user search + filter, although it can be different. E.g. "mail" or "uid" + or "userPrincipalName". The value of this field is case-sensitive + and must match the case of the attribute name returned by + the LDAP server in the user's entry. Distinguished names + can be used by specifying lower-case "dn". When this field + is set to "dn" then the LDAPIdentityProviderUserSearch's + Filter field cannot be blank, since the default value of + "dn={}" would not work. minLength: 1 type: string type: object base: - description: Base is the DN that should be used as the search - base when searching for users. E.g. "ou=users,dc=example,dc=com". + 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". minLength: 1 type: string filter: description: Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - in the filter and will be dynamically replaced by the username - for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". - For more information about LDAP filters, see https://ldap.com/ldap-filters. - 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. + 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 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 cc33968f..d901b8be 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -734,7 +734,43 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full dn (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-ldapidentityprovidergroupsearch"] +==== LDAPIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (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 LDAP provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the LDAP 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 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 "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes[$$LDAPIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each LDAP entry which was found as the result of the group search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes"] +==== LDAPIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP 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 LDAP server in the user's entry. 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 "cn" (common name). |=== @@ -757,6 +793,7 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. |=== @@ -791,8 +828,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. 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". +| *`filter`* __string__ | Filter is the LDAP 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 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-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== @@ -810,7 +847,7 @@ 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, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`username`* __string__ | Username specifies the name of the attribute in the LDAP entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. | *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== 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 d718ba65..0e28234d 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -39,14 +39,14 @@ type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // 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 LDAPIdentityProviderUserSearchAttributes struct { - // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // Username specifies the name of the attribute in the LDAP entry 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 @@ -64,14 +64,26 @@ type LDAPIdentityProviderUserSearchAttributes struct { UID string `json:"uid,omitempty"` } +type LDAPIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the LDAP 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 LDAP + // server in the user's entry. 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 "cn" (common name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + type LDAPIdentityProviderUserSearch struct { - // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // 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". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" - // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // 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 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 @@ -85,6 +97,30 @@ type LDAPIdentityProviderUserSearch struct { Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } +type LDAPIdentityProviderGroupSearch 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 LDAP provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the LDAP 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 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each LDAP entry which was found as + // the result of the group search. + // +optional + Attributes LDAPIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. @@ -100,6 +136,9 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. + GroupSearch LDAPIdentityProviderGroupSearch `json:"groupSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index c48c570f..f7762b09 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 @@ -72,6 +72,39 @@ func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearch) DeepCopyInto(out *LDAPIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearch. +func (in *LDAPIdentityProviderGroupSearch) DeepCopy() *LDAPIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearchAttributes. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopy() *LDAPIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -115,6 +148,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) } out.Bind = in.Bind out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch return } diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d396129d..46fbe1d0 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -63,13 +63,55 @@ spec: LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". - The password must be non-empty. + 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 the LDAP provider. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each LDAP entry which was found as the result + of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the LDAP 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 LDAP server + in the user's entry. 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 "cn" (common 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 LDAP provider. + Also, when not specified, the values of Filter and Attributes + are ignored. + type: string + filter: + description: Filter is the LDAP 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 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 "member={}". + type: string + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' @@ -104,37 +146,39 @@ spec: 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, - although it can be different. E.g. "mail" or "uid" or "userPrincipalName". - The value of this field is case-sensitive and must match - the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearch's Filter field cannot - be blank, since the default value of "dn={}" would not work. + description: Username specifies the name of the attribute + in the LDAP entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in the user search + filter, although it can be different. E.g. "mail" or "uid" + or "userPrincipalName". The value of this field is case-sensitive + and must match the case of the attribute name returned by + the LDAP server in the user's entry. Distinguished names + can be used by specifying lower-case "dn". When this field + is set to "dn" then the LDAPIdentityProviderUserSearch's + Filter field cannot be blank, since the default value of + "dn={}" would not work. minLength: 1 type: string type: object base: - description: Base is the DN that should be used as the search - base when searching for users. E.g. "ou=users,dc=example,dc=com". + 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". minLength: 1 type: string filter: description: Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - in the filter and will be dynamically replaced by the username - for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". - For more information about LDAP filters, see https://ldap.com/ldap-filters. - 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. + 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 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 d0effe28..0e1f160b 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -734,7 +734,43 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full dn (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-ldapidentityprovidergroupsearch"] +==== LDAPIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (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 LDAP provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the LDAP 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 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 "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes[$$LDAPIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each LDAP entry which was found as the result of the group search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes"] +==== LDAPIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP 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 LDAP server in the user's entry. 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 "cn" (common name). |=== @@ -757,6 +793,7 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. |=== @@ -791,8 +828,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. 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". +| *`filter`* __string__ | Filter is the LDAP 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 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-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== @@ -810,7 +847,7 @@ 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, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`username`* __string__ | Username specifies the name of the attribute in the LDAP entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. | *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== 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 d718ba65..0e28234d 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -39,14 +39,14 @@ type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // 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 LDAPIdentityProviderUserSearchAttributes struct { - // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // Username specifies the name of the attribute in the LDAP entry 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 @@ -64,14 +64,26 @@ type LDAPIdentityProviderUserSearchAttributes struct { UID string `json:"uid,omitempty"` } +type LDAPIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the LDAP 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 LDAP + // server in the user's entry. 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 "cn" (common name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + type LDAPIdentityProviderUserSearch struct { - // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // 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". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" - // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // 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 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 @@ -85,6 +97,30 @@ type LDAPIdentityProviderUserSearch struct { Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } +type LDAPIdentityProviderGroupSearch 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 LDAP provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the LDAP 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 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each LDAP entry which was found as + // the result of the group search. + // +optional + Attributes LDAPIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. @@ -100,6 +136,9 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. + GroupSearch LDAPIdentityProviderGroupSearch `json:"groupSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index c48c570f..f7762b09 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 @@ -72,6 +72,39 @@ func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearch) DeepCopyInto(out *LDAPIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearch. +func (in *LDAPIdentityProviderGroupSearch) DeepCopy() *LDAPIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearchAttributes. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopy() *LDAPIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -115,6 +148,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) } out.Bind = in.Bind out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch return } diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d396129d..46fbe1d0 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -63,13 +63,55 @@ spec: LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". - The password must be non-empty. + 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 the LDAP provider. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each LDAP entry which was found as the result + of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the LDAP 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 LDAP server + in the user's entry. 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 "cn" (common 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 LDAP provider. + Also, when not specified, the values of Filter and Attributes + are ignored. + type: string + filter: + description: Filter is the LDAP 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 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 "member={}". + type: string + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' @@ -104,37 +146,39 @@ spec: 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, - although it can be different. E.g. "mail" or "uid" or "userPrincipalName". - The value of this field is case-sensitive and must match - the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearch's Filter field cannot - be blank, since the default value of "dn={}" would not work. + description: Username specifies the name of the attribute + in the LDAP entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in the user search + filter, although it can be different. E.g. "mail" or "uid" + or "userPrincipalName". The value of this field is case-sensitive + and must match the case of the attribute name returned by + the LDAP server in the user's entry. Distinguished names + can be used by specifying lower-case "dn". When this field + is set to "dn" then the LDAPIdentityProviderUserSearch's + Filter field cannot be blank, since the default value of + "dn={}" would not work. minLength: 1 type: string type: object base: - description: Base is the DN that should be used as the search - base when searching for users. E.g. "ou=users,dc=example,dc=com". + 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". minLength: 1 type: string filter: description: Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - in the filter and will be dynamically replaced by the username - for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". - For more information about LDAP filters, see https://ldap.com/ldap-filters. - 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. + 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 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 348c55d0..103b809c 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -734,7 +734,43 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". The password must be non-empty. +| *`secretName`* __string__ | SecretName contains the name of a namespace-local Secret object that provides the username and password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value should be the full dn (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-ldapidentityprovidergroupsearch"] +==== LDAPIdentityProviderGroupSearch + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderspec[$$LDAPIdentityProviderSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`base`* __string__ | Base is the dn (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 LDAP provider. Also, when not specified, the values of Filter and Attributes are ignored. +| *`filter`* __string__ | Filter is the LDAP 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 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 "member={}". +| *`attributes`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes[$$LDAPIdentityProviderGroupSearchAttributes$$]__ | Attributes specifies how the group's information should be read from each LDAP entry which was found as the result of the group search. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearchattributes"] +==== LDAPIdentityProviderGroupSearchAttributes + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP 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 LDAP server in the user's entry. 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 "cn" (common name). |=== @@ -757,6 +793,7 @@ Spec for configuring an LDAP identity provider. | *`tls`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-tlsspec[$$TLSSpec$$]__ | TLS contains the connection settings for how to establish the connection to the Host. | *`bind`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderbind[$$LDAPIdentityProviderBind$$]__ | Bind contains the configuration for how to provide access credentials during an initial bind to the LDAP server to be allowed to perform searches and binds to validate a user's credentials during a user's authentication attempt. | *`userSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityproviderusersearch[$$LDAPIdentityProviderUserSearch$$]__ | UserSearch contains the configuration for searching for a user by name in the LDAP provider. +| *`groupSearch`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-idp-v1alpha1-ldapidentityprovidergroupsearch[$$LDAPIdentityProviderGroupSearch$$]__ | GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. |=== @@ -791,8 +828,8 @@ Status of an LDAP identity provider. [cols="25a,75a", options="header"] |=== | Field | Description -| *`base`* __string__ | Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". -| *`filter`* __string__ | Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. 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". +| *`filter`* __string__ | Filter is the LDAP 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 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-ldapidentityproviderusersearchattributes[$$LDAPIdentityProviderUserSearchAttributes$$]__ | Attributes specifies how the user's information should be read from the LDAP entry which was found as the result of the user search. |=== @@ -810,7 +847,7 @@ 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, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. +| *`username`* __string__ | Username specifies the name of the attribute in the LDAP entry whose value shall become the username of the user after a successful authentication. This would typically be the same attribute name used in the user search filter, although it can be different. E.g. "mail" or "uid" or "userPrincipalName". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". When this field is set to "dn" then the LDAPIdentityProviderUserSearch's Filter field cannot be blank, since the default value of "dn={}" would not work. | *`uid`* __string__ | UID specifies the name of the attribute in the LDAP entry which whose value shall be used to uniquely identify the user within this LDAP provider after a successful authentication. E.g. "uidNumber" or "objectGUID". The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". |=== 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 d718ba65..0e28234d 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -39,14 +39,14 @@ type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // 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 LDAPIdentityProviderUserSearchAttributes struct { - // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // Username specifies the name of the attribute in the LDAP entry 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 @@ -64,14 +64,26 @@ type LDAPIdentityProviderUserSearchAttributes struct { UID string `json:"uid,omitempty"` } +type LDAPIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the LDAP 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 LDAP + // server in the user's entry. 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 "cn" (common name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + type LDAPIdentityProviderUserSearch struct { - // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // 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". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" - // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // 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 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 @@ -85,6 +97,30 @@ type LDAPIdentityProviderUserSearch struct { Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } +type LDAPIdentityProviderGroupSearch 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 LDAP provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the LDAP 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 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each LDAP entry which was found as + // the result of the group search. + // +optional + Attributes LDAPIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. @@ -100,6 +136,9 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. + GroupSearch LDAPIdentityProviderGroupSearch `json:"groupSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index c48c570f..f7762b09 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 @@ -72,6 +72,39 @@ func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearch) DeepCopyInto(out *LDAPIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearch. +func (in *LDAPIdentityProviderGroupSearch) DeepCopy() *LDAPIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearchAttributes. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopy() *LDAPIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -115,6 +148,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) } out.Bind = in.Bind out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch return } diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index d396129d..46fbe1d0 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -63,13 +63,55 @@ spec: LDAP bind user. This account will be used to perform LDAP searches. The Secret should be of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". - The password must be non-empty. + 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 the LDAP provider. + properties: + attributes: + description: Attributes specifies how the group's information + should be read from each LDAP entry which was found as the result + of the group search. + properties: + groupName: + description: GroupName specifies the name of the attribute + in the LDAP 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 LDAP server + in the user's entry. 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 "cn" (common 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 LDAP provider. + Also, when not specified, the values of Filter and Attributes + are ignored. + type: string + filter: + description: Filter is the LDAP 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 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 "member={}". + type: string + type: object host: description: 'Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636.' @@ -104,37 +146,39 @@ spec: 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, - although it can be different. E.g. "mail" or "uid" or "userPrincipalName". - The value of this field is case-sensitive and must match - the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". When this field is set to "dn" - then the LDAPIdentityProviderUserSearch's Filter field cannot - be blank, since the default value of "dn={}" would not work. + description: Username specifies the name of the attribute + in the LDAP entry whose value shall become the username + of the user after a successful authentication. This would + typically be the same attribute name used in the user search + filter, although it can be different. E.g. "mail" or "uid" + or "userPrincipalName". The value of this field is case-sensitive + and must match the case of the attribute name returned by + the LDAP server in the user's entry. Distinguished names + can be used by specifying lower-case "dn". When this field + is set to "dn" then the LDAPIdentityProviderUserSearch's + Filter field cannot be blank, since the default value of + "dn={}" would not work. minLength: 1 type: string type: object base: - description: Base is the DN that should be used as the search - base when searching for users. E.g. "ou=users,dc=example,dc=com". + 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". minLength: 1 type: string filter: description: Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - in the filter and will be dynamically replaced by the username - for which the search is being run. E.g. "mail={}" or "&(objectClass=person)(uid={})". - For more information about LDAP filters, see https://ldap.com/ldap-filters. - 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. + 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 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 d718ba65..0e28234d 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -39,14 +39,14 @@ type LDAPIdentityProviderBind struct { // SecretName contains the name of a namespace-local Secret object that provides the username and // password for an LDAP bind user. This account will be used to perform LDAP searches. The Secret should be // of type "kubernetes.io/basic-auth" which includes "username" and "password" keys. The username value - // should be the full DN of your bind account, e.g. "cn=bind-account,ou=users,dc=example,dc=com". + // 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 LDAPIdentityProviderUserSearchAttributes struct { - // Username specifies the name of attribute in the LDAP entry which whose value shall become the username + // Username specifies the name of the attribute in the LDAP entry 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 @@ -64,14 +64,26 @@ type LDAPIdentityProviderUserSearchAttributes struct { UID string `json:"uid,omitempty"` } +type LDAPIdentityProviderGroupSearchAttributes struct { + // GroupName specifies the name of the attribute in the LDAP 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 LDAP + // server in the user's entry. 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 "cn" (common name). + // +optional + GroupName string `json:"groupName,omitempty"` +} + type LDAPIdentityProviderUserSearch struct { - // Base is the DN that should be used as the search base when searching for users. E.g. "ou=users,dc=example,dc=com". + // 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". // +kubebuilder:validation:MinLength=1 Base string `json:"base,omitempty"` // Filter is the LDAP search filter which should be applied when searching for users. The pattern "{}" must occur - // in the filter and will be dynamically replaced by the username for which the search is being run. E.g. "mail={}" - // or "&(objectClass=person)(uid={})". For more information about LDAP filters, see https://ldap.com/ldap-filters. + // 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 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 @@ -85,6 +97,30 @@ type LDAPIdentityProviderUserSearch struct { Attributes LDAPIdentityProviderUserSearchAttributes `json:"attributes,omitempty"` } +type LDAPIdentityProviderGroupSearch 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 LDAP provider. Also, when not specified, + // the values of Filter and Attributes are ignored. + // +optional + Base string `json:"base,omitempty"` + + // Filter is the LDAP 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 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 "member={}". + // +optional + Filter string `json:"filter,omitempty"` + + // Attributes specifies how the group's information should be read from each LDAP entry which was found as + // the result of the group search. + // +optional + Attributes LDAPIdentityProviderGroupSearchAttributes `json:"attributes,omitempty"` +} + // Spec for configuring an LDAP identity provider. type LDAPIdentityProviderSpec struct { // Host is the hostname of this LDAP identity provider, i.e., where to connect. For example: ldap.example.com:636. @@ -100,6 +136,9 @@ type LDAPIdentityProviderSpec struct { // UserSearch contains the configuration for searching for a user by name in the LDAP provider. UserSearch LDAPIdentityProviderUserSearch `json:"userSearch,omitempty"` + + // GroupSearch contains the configuration for searching for a user's group membership in the LDAP provider. + GroupSearch LDAPIdentityProviderGroupSearch `json:"groupSearch,omitempty"` } // LDAPIdentityProvider describes the configuration of an upstream Lightweight Directory Access diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go index c48c570f..f7762b09 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/zz_generated.deepcopy.go @@ -72,6 +72,39 @@ func (in *LDAPIdentityProviderBind) DeepCopy() *LDAPIdentityProviderBind { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearch) DeepCopyInto(out *LDAPIdentityProviderGroupSearch) { + *out = *in + out.Attributes = in.Attributes + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearch. +func (in *LDAPIdentityProviderGroupSearch) DeepCopy() *LDAPIdentityProviderGroupSearch { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearch) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopyInto(out *LDAPIdentityProviderGroupSearchAttributes) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LDAPIdentityProviderGroupSearchAttributes. +func (in *LDAPIdentityProviderGroupSearchAttributes) DeepCopy() *LDAPIdentityProviderGroupSearchAttributes { + if in == nil { + return nil + } + out := new(LDAPIdentityProviderGroupSearchAttributes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LDAPIdentityProviderList) DeepCopyInto(out *LDAPIdentityProviderList) { *out = *in @@ -115,6 +148,7 @@ func (in *LDAPIdentityProviderSpec) DeepCopyInto(out *LDAPIdentityProviderSpec) } out.Bind = in.Bind out.UserSearch = in.UserSearch + out.GroupSearch = in.GroupSearch return } diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index 5484e424..81837769 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -160,6 +160,11 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream * 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, } diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go index 9b0137d2..99f09f1e 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher_test.go @@ -145,16 +145,19 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { 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" - testUsernameAttrName = "test-username-attr" - testUIDAttrName = "test-uid-attr" + 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)} @@ -178,6 +181,13 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { UID: testUIDAttrName, }, }, + GroupSearch: v1alpha1.LDAPIdentityProviderGroupSearch{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + Attributes: v1alpha1.LDAPIdentityProviderGroupSearchAttributes{ + GroupName: testGroupNameAttrName, + }, + }, }, } editedValidUpstream := func(editFunc func(*v1alpha1.LDAPIdentityProvider)) *v1alpha1.LDAPIdentityProvider { @@ -198,6 +208,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { UsernameAttribute: testUsernameAttrName, UIDAttribute: testUIDAttrName, }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, } bindSecretValidTrueCondition := func(gen int64) v1alpha1.Condition { @@ -438,6 +453,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { UsernameAttribute: testUsernameAttrName, UIDAttribute: testUIDAttrName, }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, }, }, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ @@ -484,6 +504,11 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) { UsernameAttribute: testUsernameAttrName, UIDAttribute: testUIDAttrName, }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupNameAttrName, + }, }, }, wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ diff --git a/internal/mocks/mockldapconn/mockldapconn.go b/internal/mocks/mockldapconn/mockldapconn.go index a96cf79c..24b0b1aa 100644 --- a/internal/mocks/mockldapconn/mockldapconn.go +++ b/internal/mocks/mockldapconn/mockldapconn.go @@ -78,3 +78,18 @@ func (mr *MockConnMockRecorder) Search(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockConn)(nil).Search), arg0) } + +// SearchWithPaging mocks base method. +func (m *MockConn) SearchWithPaging(arg0 *ldap.SearchRequest, arg1 uint32) (*ldap.SearchResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchWithPaging", arg0, arg1) + ret0, _ := ret[0].(*ldap.SearchResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SearchWithPaging indicates an expected call of SearchWithPaging. +func (mr *MockConnMockRecorder) SearchWithPaging(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchWithPaging", reflect.TypeOf((*MockConn)(nil).SearchWithPaging), arg0, arg1) +} diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 614a101e..2ffec1ed 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "net" + "sort" "strings" "time" @@ -24,9 +25,11 @@ import ( ) const ( - ldapsScheme = "ldaps" - distinguishedNameAttributeName = "dn" - userSearchFilterInterpolationLocationMarker = "{}" + ldapsScheme = "ldaps" + distinguishedNameAttributeName = "dn" + commonNameAttributeName = "cn" + searchFilterInterpolationLocationMarker = "{}" + groupSearchPageSize = uint32(250) ) // Conn abstracts the upstream LDAP communication protocol (mostly for testing). @@ -35,6 +38,8 @@ type Conn interface { Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) + SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) + Close() } @@ -78,6 +83,9 @@ type ProviderConfig struct { // UserSearch contains information about how to search for users in the upstream LDAP IDP. UserSearch UserSearchConfig + // GroupSearch contains information about how to search for group membership in the upstream LDAP IDP. + GroupSearch GroupSearchConfig + // Dialer exists to enable testing. When nil, will use a default appropriate for production use. Dialer LDAPDialer } @@ -99,6 +107,20 @@ type UserSearchConfig struct { UIDAttribute string } +// GroupSearchConfig contains information about how to search for group membership for users in the upstream LDAP IDP. +type GroupSearchConfig struct { + // Base is the base DN to use for the group search in the upstream LDAP IDP. Empty means to skip group search + // entirely, in which case authenticated users will not belong to any groups from the upstream LDAP IDP. + Base string + + // Filter is the filter to use for the group search in the upstream LDAP 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 } @@ -257,7 +279,7 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi return nil, false, fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err) } - mappedUsername, mappedUID, err := p.searchAndBindUser(conn, username, bindFunc) + mappedUsername, mappedUID, mappedGroupNames, err := p.searchAndBindUser(conn, username, bindFunc) if err != nil { p.traceAuthFailure(t, err) return nil, false, err @@ -272,13 +294,39 @@ func (p *Provider) authenticateUserImpl(ctx context.Context, username string, bi User: &user.DefaultInfo{ Name: mappedUsername, UID: mappedUID, - Groups: []string{}, // Support for group search coming soon. + 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 = commonNameAttributeName + } + + 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 { if p.c.UserSearch.UsernameAttribute == distinguishedNameAttributeName && len(p.c.UserSearch.Filter) == 0 { // LDAP search filters do not allow searching by DN, so we would have no reasonable default for Filter. @@ -287,36 +335,45 @@ func (p *Provider) validateConfig() error { return nil } -func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, error) { +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 { - return "", "", fmt.Errorf(`error searching for user "%s": %w`, username, err) + return "", "", nil, fmt.Errorf(`error searching for user "%s": %w`, username, err) } if len(searchResult.Entries) == 0 { plog.Debug("error finding user: user not found (if this username is valid, please check the user search configuration)", "upstreamName", p.GetName(), "username", username) - return "", "", nil + return "", "", nil, nil } if len(searchResult.Entries) > 1 { - return "", "", fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`, + return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`, username, len(searchResult.Entries), ) } userEntry := searchResult.Entries[0] if len(userEntry.DN) == 0 { - return "", "", fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username) + return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in search result without DN`, username) } mappedUsername, err := p.getSearchResultAttributeValue(p.c.UserSearch.UsernameAttribute, userEntry, username) if err != nil { - return "", "", err + return "", "", nil, err } mappedUID, err := p.getSearchResultAttributeValue(p.c.UserSearch.UIDAttribute, userEntry, username) if err != nil { - return "", "", err + return "", "", nil, err } + mappedGroupNames := []string{} + if len(p.c.GroupSearch.Base) > 0 { + mappedGroupNames, err = p.searchGroupsForUserDN(conn, userEntry.DN) + if err != nil { + return "", "", nil, err + } + } + 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 { @@ -324,12 +381,12 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c err, "upstreamName", p.GetName(), "username", username, "dn", userEntry.DN) ldapErr := &ldap.Error{} if errors.As(err, &ldapErr) && ldapErr.ResultCode == ldap.LDAPResultInvalidCredentials { - return "", "", nil + return "", "", nil, nil } - return "", "", fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) + return "", "", nil, fmt.Errorf(`error binding for user "%s" using provided password against DN "%s": %w`, username, userEntry.DN, err) } - return mappedUsername, mappedUID, nil + return mappedUsername, mappedUID, mappedGroupNames, nil } func (p *Provider) userSearchRequest(username string) *ldap.SearchRequest { @@ -347,6 +404,21 @@ func (p *Provider) userSearchRequest(username string) *ldap.SearchRequest { } } +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.c.UserSearch.UsernameAttribute != distinguishedNameAttributeName { @@ -358,12 +430,34 @@ func (p *Provider) userSearchRequestedAttributes() []string { return attributes } +func (p *Provider) groupSearchRequestedAttributes() []string { + switch p.c.GroupSearch.GroupNameAttribute { + case "": + return []string{commonNameAttributeName} + case distinguishedNameAttributeName: + return []string{} + default: + return []string{p.c.GroupSearch.GroupNameAttribute} + } +} + func (p *Provider) userSearchFilter(username string) string { safeUsername := p.escapeUsernameForSearchFilter(username) if len(p.c.UserSearch.Filter) == 0 { return fmt.Sprintf("(%s=%s)", p.c.UserSearch.UsernameAttribute, safeUsername) } - filter := strings.ReplaceAll(p.c.UserSearch.Filter, userSearchFilterInterpolationLocationMarker, 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 } @@ -375,12 +469,12 @@ func (p *Provider) escapeUsernameForSearchFilter(username string) string { return ldap.EscapeFilter(username) } -func (p *Provider) getSearchResultAttributeValue(attributeName string, fromUserEntry *ldap.Entry, username string) (string, error) { +func (p *Provider) getSearchResultAttributeValue(attributeName string, entry *ldap.Entry, username string) (string, error) { if attributeName == distinguishedNameAttributeName { - return fromUserEntry.DN, nil + return entry.DN, nil } - attributeValues := fromUserEntry.GetAttributeValues(attributeName) + 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`, diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index b4e64cce..365618f4 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -24,22 +24,32 @@ import ( ) const ( - testHost = "ldap.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-base-dn" - testUserSearchFilter = "some-filter={}-and-more-filter={}" - testUserSearchUsernameAttribute = "some-upstream-username-attribute" - testUserSearchUIDAttribute = "some-upstream-uid-attribute" - testSearchResultDNValue = "some-upstream-user-dn" - testSearchResultUsernameAttributeValue = "some-upstream-username-value" - testSearchResultUIDAttributeValue = "some-upstream-uid-value" + testHost = "ldap.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 = "some-upstream-uid-attribute" + 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 = "some-upstream-uid-value" + testGroupSearchResultGroupNameAttributeValue1 = "some-upstream-group-name-value1" + testGroupSearchResultGroupNameAttributeValue2 = "some-upstream-group-name-value2" + + expectedGroupSearchPageSize = uint32(250) ) var ( - testUserSearchFilterInterpolated = fmt.Sprintf("(some-filter=%s-and-more-filter=%s)", testUpstreamUsername, testUpstreamUsername) + 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) { @@ -56,6 +66,11 @@ func TestEndUserAuthentication(t *testing.T) { UsernameAttribute: testUserSearchUsernameAttribute, UIDAttribute: testUserSearchUIDAttribute, }, + GroupSearch: GroupSearchConfig{ + Base: testGroupSearchBase, + Filter: testGroupSearchFilter, + GroupNameAttribute: testGroupSearchGroupNameAttribute, + }, } if editFunc != nil { editFunc(config) @@ -63,7 +78,7 @@ func TestEndUserAuthentication(t *testing.T) { return config } - expectedSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { + expectedUserSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest { request := &ldap.SearchRequest{ BaseDN: testUserSearchBase, Scope: ldap.ScopeWholeSubtree, @@ -73,7 +88,7 @@ func TestEndUserAuthentication(t *testing.T) { TypesOnly: false, Filter: testUserSearchFilterInterpolated, Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute}, - Controls: nil, + Controls: nil, // don't need paging because we set the SizeLimit so small } if editFunc != nil { editFunc(request) @@ -81,6 +96,68 @@ func TestEndUserAuthentication(t *testing.T) { 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: testUserSearchResultUIDAttributeValue, + Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, + } + if editFunc != nil { + editFunc(u) + } + return &authenticator.Response{User: u} + } + tests := []struct { name string username string @@ -102,31 +179,15 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), - }, - }, - }, - Referrals: []string{}, // note that we are not following referrals at this time - Controls: []ldap.Control{}, // TODO are there any response controls that we need to be able to handle? - }, nil).Times(1) + conn.EXPECT().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(testSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{ - Name: testSearchResultUsernameAttributeValue, - UID: testSearchResultUIDAttributeValue, - Groups: []string{}, - }, + 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", @@ -137,29 +198,53 @@ func TestEndUserAuthentication(t *testing.T) { }), searchMocks: 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().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(testSearchResultDNValue, testUpstreamPassword).Times(1) + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) }, - wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{ - Name: testSearchResultUsernameAttributeValue, - UID: testSearchResultUIDAttributeValue, - Groups: []string{}, - }, + 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", @@ -170,30 +255,28 @@ func TestEndUserAuthentication(t *testing.T) { }), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { r.Attributes = []string{testUserSearchUIDAttribute} })).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: testSearchResultDNValue, + DN: testUserSearchResultDNValue, Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + 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(testSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{ - Name: testSearchResultDNValue, - UID: testSearchResultUIDAttributeValue, - Groups: []string{}, - }, + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Name = testUserSearchResultDNValue + }), }, { name: "when the UIDAttribute is dn", @@ -204,33 +287,92 @@ func TestEndUserAuthentication(t *testing.T) { }), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { r.Attributes = []string{testUserSearchUsernameAttribute} })).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: testSearchResultDNValue, + DN: testUserSearchResultDNValue, Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + 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(testSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{ - Name: testSearchResultUsernameAttributeValue, - UID: testSearchResultDNValue, - Groups: []string{}, - }, + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.UID = testUserSearchResultDNValue + }), }, { - name: "when Filter is blank it derives a search filter from the UsernameAttribute", + 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 empty then it defaults to cn", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "" // blank means to use 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) { @@ -238,62 +380,101 @@ func TestEndUserAuthentication(t *testing.T) { }), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(func(r *ldap.SearchRequest) { + conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) { r.Filter = "(" + testUserSearchUsernameAttribute + "=" + testUpstreamUsername + ")" - })).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), - }, - }, - }, - }, nil).Times(1) + })).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(testSearchResultDNValue, testUpstreamPassword).Times(1) - }, - wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{ - Name: testSearchResultUsernameAttributeValue, - UID: testSearchResultUIDAttributeValue, - Groups: []string{}, - }, + 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", + 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(expectedSearch(func(r *ldap.SearchRequest) { - r.Filter = fmt.Sprintf("(some-filter=%s-and-more-filter=%s)", `a&b|c\28d\29e\5cf\2ag`, `a&b|c\28d\29e\5cf\2ag`) - })).Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{ - { - DN: testSearchResultDNValue, - Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), - }, - }, - }, - }, nil).Times(1) + conn.EXPECT().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(testSearchResultDNValue, testUpstreamPassword).Times(1) + 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: testSearchResultUsernameAttributeValue, - UID: testSearchResultUIDAttributeValue, - Groups: []string{}, + Name: testUserSearchResultUsernameAttributeValue, + UID: testUserSearchResultUIDAttributeValue, + Groups: []string{"a", "b", "c"}, }, }, }, @@ -334,10 +515,24 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(nil, errors.New("some search error")).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(nil, errors.New("some user search error")).Times(1) conn.EXPECT().Close().Times(1) }, - wantError: fmt.Sprintf(`error searching for user "%s": some search error`, testUpstreamUsername), + wantError: fmt.Sprintf(`error searching for user "%s": some user search error`, testUpstreamUsername), + }, + { + 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", @@ -346,7 +541,7 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{}, }, nil).Times(1) conn.EXPECT().Close().Times(1) @@ -360,9 +555,9 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ - {DN: testSearchResultDNValue}, + {DN: testUserSearchResultDNValue}, {DN: "some-other-dn"}, }, }, nil).Times(1) @@ -377,7 +572,7 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ {DN: ""}, }, @@ -386,6 +581,39 @@ func TestEndUserAuthentication(t *testing.T) { }, 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, @@ -393,19 +621,54 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: testSearchResultDNValue, + DN: testUserSearchResultDNValue, Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + 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), + 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", @@ -414,23 +677,61 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: testSearchResultDNValue, + DN: testUserSearchResultDNValue, Attributes: []*ldap.EntryAttribute{ ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{ - testSearchResultUsernameAttributeValue, + testUserSearchResultUsernameAttributeValue, "unexpected-additional-value", }), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + 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), + 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", @@ -439,20 +740,55 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: testSearchResultDNValue, + DN: testUserSearchResultDNValue, Attributes: []*ldap.EntryAttribute{ ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{""}), - ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testSearchResultUIDAttributeValue}), + 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), + 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", @@ -461,12 +797,12 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: testSearchResultDNValue, + DN: testUserSearchResultDNValue, Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), }, }, }, @@ -482,14 +818,14 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: testSearchResultDNValue, + DN: testUserSearchResultDNValue, Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{ - testSearchResultUIDAttributeValue, + testUserSearchResultUIDAttributeValue, "unexpected-additional-value", }), }, @@ -507,12 +843,12 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) - conn.EXPECT().Search(expectedSearch(nil)).Return(&ldap.SearchResult{ + conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { - DN: testSearchResultDNValue, + DN: testUserSearchResultDNValue, Attributes: []*ldap.EntryAttribute{ - ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testSearchResultUsernameAttributeValue}), + ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{""}), }, }, @@ -529,24 +865,16 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: 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().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(testSearchResultDNValue, testUpstreamPassword).Return(errors.New("some bind error")).Times(1) + 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, testSearchResultDNValue), + 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", @@ -555,17 +883,9 @@ func TestEndUserAuthentication(t *testing.T) { providerConfig: providerConfig(nil), searchMocks: 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().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, @@ -575,7 +895,7 @@ func TestEndUserAuthentication(t *testing.T) { Err: errors.New("some bind error"), ResultCode: ldap.LDAPResultInvalidCredentials, } - conn.EXPECT().Bind(testSearchResultDNValue, testUpstreamPassword).Return(err).Times(1) + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Return(err).Times(1) }, }, { diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index d2fc5e46..98c72ec0 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -278,7 +278,7 @@ func TestE2EFullIntegration(t *testing.T) { // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands. t.Run("with Supervisor LDAP upstream IDP", func(t *testing.T) { expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue - expectedGroups := []string{} // LDAP groups are not implemented yet + expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. library.CreateTestClusterRoleBinding(t, @@ -317,6 +317,13 @@ func TestE2EFullIntegration(t *testing.T) { UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, + GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ + Base: env.SupervisorUpstreamLDAP.GroupSearchBase, + Filter: "", // use the default value of "member={}" + Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ + GroupName: "", // use the default value of "cn" + }, + }, }, idpv1alpha1.LDAPPhaseReady) // Use a specific session cache for this test. @@ -438,17 +445,10 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain( for _, g := range expectedGroups { expectedGroupsAsEmptyInterfaces = append(expectedGroupsAsEmptyInterfaces, g) } - require.Equal(t, expectedGroupsAsEmptyInterfaces, idTokenClaims[oidc.DownstreamGroupsClaim]) + require.ElementsMatch(t, expectedGroupsAsEmptyInterfaces, idTokenClaims[oidc.DownstreamGroupsClaim]) - expectedYAMLGroups := func() string { - var b strings.Builder - for _, g := range expectedGroups { - b.WriteString("\n") - b.WriteString(` - `) - b.WriteString(g) - } - return b.String() - }() + expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...) + expectedGroupsPlusAuthenticated = append(expectedGroupsPlusAuthenticated, "system:authenticated") // Confirm we are the right user according to Kube by calling the whoami API. kubectlCmd3 := exec.CommandContext(ctx, "kubectl", "create", "-f", "-", "-o", "yaml", "--kubeconfig", kubeconfigPath) @@ -456,28 +456,15 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain( kubectlCmd3.Stdin = strings.NewReader(here.Docf(` apiVersion: identity.concierge.%s/v1alpha1 kind: WhoAmIRequest - `, env.APIGroupSuffix)) + `, env.APIGroupSuffix)) kubectlOutput3, err := kubectlCmd3.CombinedOutput() require.NoError(t, err) - require.Equal(t, here.Docf(` - apiVersion: identity.concierge.%s/v1alpha1 - kind: WhoAmIRequest - metadata: - creationTimestamp: null - spec: {} - status: - kubernetesUserInfo: - user: - groups:%s - - system:authenticated - username: %s - `, env.APIGroupSuffix, expectedYAMLGroups, expectedUsername), - string(kubectlOutput3)) + whoAmI := deserializeWhoAmIRequest(t, string(kubectlOutput3), env.APIGroupSuffix) + require.Equal(t, expectedUsername, whoAmI.Status.KubernetesUserInfo.User.Username) + require.ElementsMatch(t, expectedGroupsPlusAuthenticated, whoAmI.Status.KubernetesUserInfo.User.Groups) - expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...) - expectedGroupsPlusAuthenticated = append(expectedGroupsPlusAuthenticated, "system:authenticated") // Validate that `pinniped whoami` returns the correct identity. assertWhoami( ctx, diff --git a/test/integration/ldap_client_test.go b/test/integration/ldap_client_test.go index fb49a099..b8e79979 100644 --- a/test/integration/ldap_client_test.go +++ b/test/integration/ldap_client_test.go @@ -68,7 +68,7 @@ func TestLDAPSearch(t *testing.T) { password: pinnyPassword, provider: upstreamldap.New(*providerConfig(nil)), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, }, }, { @@ -77,7 +77,7 @@ func TestLDAPSearch(t *testing.T) { password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, }, }, { @@ -86,7 +86,7 @@ func TestLDAPSearch(t *testing.T) { password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, }, }, { @@ -98,7 +98,7 @@ func TestLDAPSearch(t *testing.T) { p.UserSearch.Filter = "cn={}" })), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{}}, + User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, }, }, { @@ -109,7 +109,7 @@ func TestLDAPSearch(t *testing.T) { p.UserSearch.Filter = "(|(cn={})(mail={}))" })), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, }, }, { @@ -120,7 +120,7 @@ func TestLDAPSearch(t *testing.T) { p.UserSearch.Filter = "(|(cn={})(mail={}))" })), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, }, }, { @@ -129,7 +129,7 @@ func TestLDAPSearch(t *testing.T) { password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{}}, + User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{"ball-game-players", "seals"}}, }, }, { @@ -138,7 +138,7 @@ func TestLDAPSearch(t *testing.T) { password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{}}, + User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{"ball-game-players", "seals"}}, }, }, { @@ -147,7 +147,7 @@ func TestLDAPSearch(t *testing.T) { password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })), wantAuthResponse: &authenticator.Response{ - User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{}}, // note that the final answer has case preserved from the entry + User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, // note that the final answer has case preserved from the entry }, }, { @@ -160,6 +160,75 @@ func TestLDAPSearch(t *testing.T) { })), wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`, }, + { + name: "group search disabled", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { + p.GroupSearch.Base = "" + })), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "group search base causes no groups to be found for user", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { + p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree + })), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, + { + name: "using dn as the group name attribute", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "dn" + })), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{ + "cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev", + "cn=seals,ou=groups,dc=pinniped,dc=dev", + }}, + }, + }, + { + name: "using some other custom group name attribute", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test + })), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"groupOfNames", "groupOfNames"}}, + }, + }, + { + name: "using a more complex group search filter", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { + p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))" + })), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"seals"}}, + }, + }, + { + name: "using a group filter which causes no groups to be found for the user", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { + p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema + })), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, + }, + }, { name: "when the bind user username is not a valid DN", username: "pinny", @@ -209,6 +278,13 @@ func TestLDAPSearch(t *testing.T) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "*" })), wantError: `error searching for user "pinny": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`, }, + { + name: "when the group search filter does not compile", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Filter = "*" })), + wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`, + }, { name: "when there are too many search results for the user", username: "pinny", @@ -291,6 +367,13 @@ func TestLDAPSearch(t *testing.T) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "SN" })), // this is case-sensitive wantError: `found 0 values for attribute "SN" while searching for user "pinny", but expected 1 result`, }, + { + name: "when the GroupNameAttribute has the wrong case", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.GroupNameAttribute = "CN" })), // this is case-sensitive + wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": found 0 values for attribute "CN" while searching for user "cn=pinny,ou=users,dc=pinniped,dc=dev", but expected 1 result`, + }, { name: "when the UsernameAttribute is DN and has the wrong case", username: "pinny", @@ -311,21 +394,44 @@ func TestLDAPSearch(t *testing.T) { wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`, }, { - name: "when the search base is invalid", + name: "when the GroupNameAttribute is DN and has the wrong case", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "DN" // dn must be lower-case + })), + wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": found 0 values for attribute "DN" while searching for user "cn=pinny,ou=users,dc=pinniped,dc=dev", but expected 1 result`, + }, + { + name: "when the user search base is invalid", username: "pinny", password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "invalid-base" })), wantError: `error searching for user "pinny": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, }, { - name: "when the search base does not exist", + name: "when the group search base is invalid", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Base = "invalid-base" })), + wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, + }, + { + name: "when the user search base does not exist", username: "pinny", password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" })), wantError: `error searching for user "pinny": LDAP Result Code 32 "No Such Object": `, }, { - name: "when the search base causes no search results", + name: "when the group search base does not exist", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" })), + wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 32 "No Such Object": `, + }, + { + name: "when the user search base causes no search results", username: "pinny", password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" })), @@ -443,6 +549,11 @@ func defaultProviderConfig(env *library.TestEnv, ldapHostPort string) *upstreaml UsernameAttribute: "cn", UIDAttribute: "uidNumber", }, + GroupSearch: upstreamldap.GroupSearchConfig{ + Base: "ou=groups,dc=pinniped,dc=dev", + Filter: "", // defaults to member={} + GroupNameAttribute: "", // defaults to cn + }, } } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index fbfa8d7b..41d97aeb 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -45,6 +45,7 @@ func TestSupervisorLogin(t *testing.T) { requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client) wantDownstreamIDTokenSubjectToMatch string wantDownstreamIDTokenUsernameToMatch string + wantDownstreamIDTokenGroups []string }{ { name: "oidc", @@ -65,9 +66,10 @@ func TestSupervisorLogin(t *testing.T) { 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=") + ".+", + wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, { - name: "ldap with email as username", + name: "ldap with email as username and groups names as DNs", createIDP: func(t *testing.T) { t.Helper() secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, @@ -92,6 +94,13 @@ func TestSupervisorLogin(t *testing.T) { 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"]`, @@ -114,9 +123,10 @@ func TestSupervisorLogin(t *testing.T) { ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), + wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { - name: "ldap with CN as username ", // try another variation of configuration options + name: "ldap with CN as username and group names as CNs", // try another variation of configuration options createIDP: func(t *testing.T) { t.Helper() secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, @@ -141,6 +151,13 @@ func TestSupervisorLogin(t *testing.T) { UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, + GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{ + Base: env.SupervisorUpstreamLDAP.GroupSearchBase, + Filter: "", + Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ + GroupName: "cn", + }, + }, }, idpv1alpha1.LDAPPhaseReady) expectedMsg := fmt.Sprintf( `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, @@ -163,6 +180,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.SupervisorUpstreamLDAP.TestUserDN), + wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs, }, } for _, test := range tests { @@ -173,6 +191,7 @@ func TestSupervisorLogin(t *testing.T) { test.requestAuthorization, test.wantDownstreamIDTokenSubjectToMatch, test.wantDownstreamIDTokenUsernameToMatch, + test.wantDownstreamIDTokenGroups, ) }) } @@ -207,7 +226,7 @@ func testSupervisorLogin( t *testing.T, createIDP func(t *testing.T), requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client), - wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, + wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string, ) { env := library.IntegrationEnv(t) @@ -350,7 +369,7 @@ func testSupervisorLogin( expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"} verifyTokenResponse(t, tokenResponse, discovery, downstreamOAuth2Config, nonceParam, - expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch) + expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups) // token exchange on the original token doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery) @@ -363,7 +382,7 @@ func testSupervisorLogin( expectedIDTokenClaims = append(expectedIDTokenClaims, "at_hash") verifyTokenResponse(t, refreshedTokenResponse, discovery, downstreamOAuth2Config, "", - expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch) + expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups) require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken) require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken) @@ -380,7 +399,7 @@ func verifyTokenResponse( downstreamOAuth2Config oauth2.Config, nonceParam nonce.Nonce, expectedIDTokenClaims []string, - wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, + wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string, ) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -415,6 +434,9 @@ func verifyTokenResponse( // Check username claim of the ID token. require.Regexp(t, wantDownstreamIDTokenUsernameToMatch, idTokenClaims["username"].(string)) + // Check the groups claim. + require.ElementsMatch(t, wantDownstreamIDTokenGroups, idTokenClaims["groups"]) + // Some light verification of the other tokens that were returned. require.NotEmpty(t, tokenResponse.AccessToken) require.Equal(t, "bearer", tokenResponse.TokenType) diff --git a/test/library/env.go b/test/library/env.go index 0e730d05..e4103843 100644 --- a/test/library/env.go +++ b/test/library/env.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "io/ioutil" "os" + "sort" "strings" "sync" "testing" @@ -71,18 +72,21 @@ type TestOIDCUpstream struct { } type TestLDAPUpstream struct { - Host string `json:"host"` - CABundle string `json:"caBundle"` - BindUsername string `json:"bindUsername"` - BindPassword string `json:"bindPassword"` - UserSearchBase string `json:"userSearchBase"` - TestUserDN string `json:"testUserDN"` - TestUserCN string `json:"testUserCN"` - TestUserPassword string `json:"testUserPassword"` - TestUserMailAttributeName string `json:"testUserMailAttributeName"` - TestUserMailAttributeValue string `json:"testUserMailAttributeValue"` - TestUserUniqueIDAttributeName string `json:"testUserUniqueIDAttributeName"` - TestUserUniqueIDAttributeValue string `json:"testUserUniqueIDAttributeValue"` + Host string `json:"host"` + 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" } // ProxyEnv returns a set of environment variable strings (e.g., to combine with os.Environ()) which set up the configured test HTTP proxy. @@ -240,14 +244,20 @@ func loadEnvVars(t *testing.T, result *TestEnv) { BindUsername: needEnv(t, "PINNIPED_TEST_LDAP_BIND_ACCOUNT_USERNAME"), BindPassword: needEnv(t, "PINNIPED_TEST_LDAP_BIND_ACCOUNT_PASSWORD"), UserSearchBase: needEnv(t, "PINNIPED_TEST_LDAP_USERS_SEARCH_BASE"), + GroupSearchBase: needEnv(t, "PINNIPED_TEST_LDAP_GROUPS_SEARCH_BASE"), TestUserDN: needEnv(t, "PINNIPED_TEST_LDAP_USER_DN"), TestUserCN: needEnv(t, "PINNIPED_TEST_LDAP_USER_CN"), TestUserUniqueIDAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_NAME"), TestUserUniqueIDAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_VALUE"), TestUserMailAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME"), TestUserMailAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE"), + 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"), } + + sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) + sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) } func (e *TestEnv) HasCapability(cap Capability) bool {