From f79db588be089909e8b430a928213eb104ae2972 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sun, 18 Jun 2023 14:40:38 +1000 Subject: [PATCH] feat(authentication): ldap memberof group search (#5418) Introduces the concept of group search mode into the LDAP configuration. This also adds the filter and memberof search modes. The full description of these is included in the docs but the filter mode is the same mode as previous which is also the default and recommended value. The memberof mode should only be used by users who are aware of how the concept works as per the docs. Closes #2161 Signed-off-by: James Elliott --- config.template.yml | 55 +- .../en/configuration/first-factor/ldap.md | 132 +- .../en/integration/deployment/docker.md | 48 +- .../en/integration/ldap/introduction.md | 30 +- docs/content/en/reference/guides/ldap.md | 114 +- docs/data/configkeys.json | 2 +- internal/authentication/const.go | 19 +- internal/authentication/gen.go | 4 +- ...ck.go => ldap_client_factory_mock_test.go} | 0 ...lient_mock.go => ldap_client_mock_test.go} | 0 .../authentication/ldap_control_types_test.go | 17 + internal/authentication/ldap_user_provider.go | 231 +- .../ldap_user_provider_startup.go | 55 +- .../authentication/ldap_user_provider_test.go | 2391 +++++++++++++---- internal/authentication/ldap_util.go | 10 +- internal/authentication/ldap_util_test.go | 6 + internal/authentication/types.go | 1 + internal/configuration/config.template.yml | 55 +- internal/configuration/deprecation.go | 32 + internal/configuration/provider_test.go | 22 + .../configuration/schema/authentication.go | 124 +- internal/configuration/schema/const.go | 25 +- internal/configuration/schema/keys.go | 11 +- .../test_resources/config.durations.yml | 7 +- .../test_resources/config.filtered.yml | 7 +- .../configuration/test_resources/config.yml | 7 +- .../test_resources/config_alt.yml | 8 +- .../test_resources/config_bad_keys.yml | 7 +- .../config_domain_bad_regex.yml | 7 +- .../test_resources/config_domain_regex.yml | 8 +- .../test_resources/config_oidc.yml | 7 +- .../config_oidc_disable_entropy.yml | 137 + .../test_resources/config_oidc_modern.yml | 133 +- .../config_smtp_sender_blank.yml | 7 +- .../config_smtp_sender_invalid.yml | 7 +- .../test_resources/config_with_secret.yml | 8 +- .../configuration/validator/authentication.go | 74 +- .../validator/authentication_test.go | 507 ++-- internal/configuration/validator/const.go | 37 +- .../configuration/validator/const_test.go | 5 + .../suites/HighAvailability/configuration.yml | 11 +- internal/suites/LDAP/configuration.yml | 11 +- .../kube/authelia/configs/configuration.yml | 11 +- 43 files changed, 3160 insertions(+), 1230 deletions(-) rename internal/authentication/{ldap_client_factory_mock.go => ldap_client_factory_mock_test.go} (100%) rename internal/authentication/{ldap_client_mock.go => ldap_client_mock_test.go} (100%) create mode 100644 internal/authentication/ldap_control_types_test.go create mode 100644 internal/configuration/test_resources/config_oidc_disable_entropy.yml diff --git a/config.template.yml b/config.template.yml index 61e010094..f4cedef98 100644 --- a/config.template.yml +++ b/config.template.yml @@ -315,7 +315,7 @@ authentication_backend: ## because it allows Authelia to offload the stateful operations ## onto the LDAP service. # ldap: - ## The address of the LDAP server to connect to in the address common syntax. + ## The address of the directory server to connect to in the address common syntax. ## Format: [://][:]. ## Square brackets indicate optional portions of the format. Scheme must be 'ldap', 'ldaps', or 'ldapi`. ## The default scheme is 'ldapi' if the address is an absolute path otherwise it's 'ldaps'. @@ -401,16 +401,6 @@ authentication_backend: ## See also: additional_users_dn, additional_groups_dn. # base_dn: 'dc=example,dc=com' - ## The attribute holding the username of the user. This attribute is used to populate the username in the session - ## information. For your information, Microsoft Active Directory usually uses 'sAMAccountName' and OpenLDAP usually - ## uses 'uid'. Beware that this attribute holds the unique identifiers for the users binding the user and the - ## configuration stored in database. Therefore only single value attributes are allowed and the value must never be - ## changed once attributed to a user otherwise it would break the configuration for that user. Technically, - ## non-unique attributes like 'mail' can also be used but we don't recommend using them, we instead advise to use - ## a filter to perform alternative lookups and the attributes mentioned above (sAMAccountName and uid) to - ## follow https://datatracker.ietf.org/doc/html/rfc2307. - # username_attribute: 'uid' - ## The additional_users_dn is prefixed to base_dn and delimited by a comma when searching for users. ## i.e. with this set to OU=Users and base_dn set to DC=a,DC=com; OU=Users,DC=a,DC=com is searched for users. # additional_users_dn: 'ou=users' @@ -441,15 +431,9 @@ authentication_backend: ## (&(uniqueMember={dn})(objectClass=groupOfUniqueNames)) # groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - ## The attribute holding the name of the group. - # group_name_attribute: 'cn' - - ## The attribute holding the mail address of the user. If multiple email addresses are defined for a user, only the - ## first one returned by the LDAP server is used. - # mail_attribute: 'mail' - - ## The attribute holding the display name of the user. This will be used to greet an authenticated user. - # display_name_attribute: 'displayName' + ## The group search mode to use. Options are 'filter' or 'memberof'. It's essential to read the docs if you wish to + ## use 'memberof'. Also 'filter' is the best choice for most use cases. + # group_search_mode: 'filter' ## Follow referrals returned by the server. ## This is especially useful for environments where read-only servers exist. Only implemented for write operations. @@ -460,6 +444,37 @@ authentication_backend: ## Password can also be set using a secret: https://www.authelia.com/c/secrets # password: 'password' + ## The attributes for users and objects from the directory server. + # attributes: + + ## The distinguished name attribute if your directory server supports it. Users should read the docs before + ## configuring. Only used for the 'memberof' group search mode. + # distinguished_name: '' + + ## The attribute holding the username of the user. This attribute is used to populate the username in the session + ## information. For your information, Microsoft Active Directory usually uses 'sAMAccountName' and OpenLDAP + ## usually uses 'uid'. Beware that this attribute holds the unique identifiers for the users binding the user and + ## the configuration stored in database; therefore only single value attributes are allowed and the value must + ## never be changed once attributed to a user otherwise it would break the configuration for that user. + ## Technically non-unique attributes like 'mail' can also be used but we don't recommend using them, we instead + ## advise to use a filter to perform alternative lookups and the attributes mentioned above + ## (sAMAccountName and uid) to follow https://datatracker.ietf.org/doc/html/rfc2307. + # username: 'uid' + + ## The attribute holding the display name of the user. This will be used to greet an authenticated user. + # display_name: 'displayName' + + ## The attribute holding the mail address of the user. If multiple email addresses are defined for a user, only + ## the first one returned by the directory server is used. + # mail: 'mail' + + ## The attribute which provides distinguished names of groups an object is a member of. + ## Only used for the 'memberof' group search mode. + # member_of: 'memberOf' + + ## The attribute holding the name of the group. + # group_name: 'cn' + ## ## File (Authentication Provider) ## diff --git a/docs/content/en/configuration/first-factor/ldap.md b/docs/content/en/configuration/first-factor/ldap.md index 5e6a7815c..fe8e1c4db 100644 --- a/docs/content/en/configuration/first-factor/ldap.md +++ b/docs/content/en/configuration/first-factor/ldap.md @@ -101,16 +101,20 @@ authentication_backend: base_dn: 'DC=example,DC=com' additional_users_dn: 'OU=users' users_filter: '(&({username_attribute}={input})(objectClass=person))' - username_attribute: 'uid' - mail_attribute: 'mail' - display_name_attribute: 'displayName' additional_groups_dn: 'OU=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' + group_search_mode: 'filter' permit_referrals: false permit_unauthenticated_bind: false user: 'CN=admin,DC=example,DC=com' password: 'password' + attributes: + distinguished_name: 'distinguishedName' + username: 'uid' + display_name: 'displayName' + mail: 'mail' + member_of: 'memberOf' + group_name: 'cn' ``` ## Options @@ -209,66 +213,33 @@ The LDAP filter to narrow down which users are valid. This is important to set c The default value is dependent on the [implementation](#implementation), refer to the [attribute defaults](../../reference/guides/ldap.md#attribute-defaults) for more information. -### username_attribute - -{{< confkey type="string" required="situational" >}} - -*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a -default negating this requirement. Refer to the [attribute defaults](../../reference/guides/ldap.md#attribute-defaults) -for more information.* - -The LDAP attribute that maps to the username in *Authelia*. This must contain the `{username_attribute}` -[placeholder](../../reference/guides/ldap.md#users-filter-replacements). - -### mail_attribute - -{{< confkey type="string" required="situational" >}} - -*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a -default negating this requirement. Refer to the [attribute defaults](../../reference/guides/ldap.md#attribute-defaults) -for more information.* - -The attribute to retrieve which contains the users email addresses. This is important for the device registration and -password reset processes. The user must have an email address in order for Authelia to perform identity verification -when a user attempts to reset their password or register a second factor device. - -### display_name_attribute - -{{< confkey type="string" required="situational" >}} - -*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a -default negating this requirement. Refer to the [attribute defaults](#attribute-defaults) for more information.* - -The attribute to retrieve which is shown on the Web UI to the user when they log in. - ### additional_groups_dn {{< confkey type="string" required="no" >}} -Similar to [additional_users_dn](#additional_users_dn) but it applies to group searches. +Similar to [additional_users_dn](#additionalusersdn) but it applies to group searches. ### groups_filter {{< confkey type="string" required="situational" >}} *__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a -default negating this requirement. Refer to the [filter defaults](#filter-defaults) for more information.* +default negating this requirement. Refer to the [filter defaults](../../reference/guides/ldap.md#filter-defaults) for +more information.* -Similar to [users_filter](#users_filter) but it applies to group searches. In order to include groups the member is not +Similar to [users_filter](#usersfilter) but it applies to group searches. In order to include groups the member is not a direct member of, but is a member of another group that is a member of those (i.e. recursive groups), you may try using the following filter which is currently only tested against Microsoft Active Directory: `(&(member:1.2.840.113556.1.4.1941:={dn})(objectClass=group)(objectCategory=group))` -### group_name_attribute +### group_search_mode -{{< confkey type="string" required="situational" >}} +{{< confkey type="string" default="filter" required="no" >}} -*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a -default negating this requirement. Refer to the [attribute defaults](#attribute-defaults) for more -information.* - -The LDAP attribute that is used by Authelia to determine the group name. +The group search mode controls how user groups are discovered. The default of `filter` directly uses the filter to +determine the result. The `memberof` experimental mode does another special filtered search. See the +[Reference Documentation](../../reference/guides/ldap.md#group-search-modes) for more information. ### permit_referrals @@ -313,6 +284,71 @@ It's __strongly recommended__ this is a [Random Alphanumeric String](../../reference/guides/generating-secure-values.md#generating-a-random-alphanumeric-string) with 64 or more characters and the user password is changed to this value. +### attributes + +The following options configure The directory server attribute mappings. + +#### distinguished_name + +{{< confkey type="string" required="situational" >}} + +*__Note:__ This option is technically not required however it is required when using the group search mode +`memberof` replacement `{memberof:dn}`.* + +The directory server attribute which contains the distinguished name, primarily used to perform filtered searches. There +is a clear distinction between the actual distinguished name and a distinguished name attribute, all directories have +distinguished names for objects, but not all have an attribute representing this that can be searched on. + +The only known support at this time is with Active Directory. + +#### username + +{{< confkey type="string" required="situational" >}} + +*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a +default negating this requirement. Refer to the [attribute defaults] for more information.* + +The directory server attribute that maps to the username in *Authelia*. This must contain the `{username_attribute}` [placeholder]. + +#### display_name + +{{< confkey type="string" required="situational" >}} + +*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a +default negating this requirement. Refer to the [attribute defaults] for more information.* + +The directory server attribute to retrieve which is shown on the Web UI to the user when they log in. + +#### mail + +{{< confkey type="string" required="situational" >}} + +*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a +default negating this requirement. Refer to the [attribute defaults] for more information.* + +The directory server attribute to retrieve which contains the users email addresses. This is important for the device +registration and password reset processes. The user must have an email address in order for Authelia to perform +identity verification when a user attempts to reset their password or register a second factor device. + +#### member_of + +{{< confkey type="string" required="situational" >}} + +*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a +default negating this requirement. Refer to the [attribute defaults] for more information.* + +The directory server attribute which contains the groups a user is a member of. This is currently only used for the +`memberof` group search mode. + +#### group_name + +{{< confkey type="string" required="situational" >}} + +*__Note:__ This option is technically required however the [implementation](#implementation) option can implicitly set a +default negating this requirement. Refer to the [attribute defaults] for more information.* + +The directory server attribute that is used by Authelia to determine the group name. + ## Refresh Interval It's recommended you either use the default [refresh interval](introduction.md#refreshinterval) or configure this to @@ -332,6 +368,8 @@ for your users. - [LDAP Reference Guide](../../reference/guides/ldap.md) -[username attribute]: #usernameattribute +[username attribute]: #username [TechNet wiki]: https://social.technet.microsoft.com/wiki/contents/articles/5392.active-directory-ldap-syntax-filters.aspx [RFC2307]: https://datatracker.ietf.org/doc/html/rfc2307 +[attribute defaults]: ../../reference/guides/ldap.md#attribute-defaults +[placeholder]: ../../reference/guides/ldap.md#users-filter-replacements diff --git a/docs/content/en/integration/deployment/docker.md b/docs/content/en/integration/deployment/docker.md index cba48efce..6e4b9b987 100644 --- a/docs/content/en/integration/deployment/docker.md +++ b/docs/content/en/integration/deployment/docker.md @@ -92,35 +92,35 @@ Use this [Standalone Example](#standalone-example) if you want to use version: "3.8" secrets: JWT_SECRET: - file: ${PWD}/data/authelia/secrets/JWT_SECRET + file: '${PWD}/data/authelia/secrets/JWT_SECRET' SESSION_SECRET: - file: ${PWD}/data/authelia/secrets/SESSION_SECRET + file: '${PWD}/data/authelia/secrets/SESSION_SECRET' STORAGE_PASSWORD: - file: ${PWD}/data/authelia/secrets/STORAGE_PASSWORD + file: '${PWD}/data/authelia/secrets/STORAGE_PASSWORD' STORAGE_ENCRYPTION_KEY: - file: ${PWD}/data/authelia/secrets/STORAGE_ENCRYPTION_KEY + file: '${PWD}/data/authelia/secrets/STORAGE_ENCRYPTION_KEY' services: authelia: - container_name: authelia - image: docker.io/authelia/authelia:latest - restart: unless-stopped + container_name: 'authelia' + image: 'docker.io/authelia/authelia:latest' + restart: 'unless-stopped' networks: net: aliases: [] expose: - 9091 - secrets: [JWT_SECRET, SESSION_SECRET, STORAGE_PASSWORD, STORAGE_ENCRYPTION_KEY] + secrets: ['JWT_SECRET', 'SESSION_SECRET', 'STORAGE_PASSWORD', 'STORAGE_ENCRYPTION_KEY'] environment: - AUTHELIA_JWT_SECRET_FILE: /run/secrets/JWT_SECRET - AUTHELIA_SESSION_SECRET_FILE: /run/secrets/SESSION_SECRET - AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE: /run/secrets/STORAGE_PASSWORD - AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /run/secrets/STORAGE_ENCRYPTION_KEY + AUTHELIA_JWT_SECRET_FILE: '/run/secrets/JWT_SECRET' + AUTHELIA_SESSION_SECRET_FILE: '/run/secrets/SESSION_SECRET' + AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE: '/run/secrets/STORAGE_PASSWORD' + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: '/run/secrets/STORAGE_ENCRYPTION_KEY' volumes: - - ${PWD}/data/authelia/config:/config + - '${PWD}/data/authelia/config:/config' networks: net: external: true - name: net + name: 'net' ... ``` {{< /details >}} @@ -136,26 +136,26 @@ Use this [Standalone Example](#standalone-example) if you want to use a standard version: "3.8" services: authelia: - container_name: authelia - image: docker.io/authelia/authelia:latest - restart: unless-stopped + container_name: 'authelia' + image: 'docker.io/authelia/authelia:latest' + restart: 'unless-stopped' networks: net: aliases: [] expose: - 9091 environment: - AUTHELIA_JWT_SECRET_FILE: /secrets/JWT_SECRET - AUTHELIA_SESSION_SECRET_FILE: /secrets/SESSION_SECRET - AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE: /secrets/STORAGE_PASSWORD - AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /secrets/STORAGE_ENCRYPTION_KEY + AUTHELIA_JWT_SECRET_FILE: '/secrets/JWT_SECRET' + AUTHELIA_SESSION_SECRET_FILE: '/secrets/SESSION_SECRET' + AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE: '/secrets/STORAGE_PASSWORD' + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: '/secrets/STORAGE_ENCRYPTION_KEY' volumes: - - ${PWD}/data/authelia/config:/config - - ${PWD}/data/authelia/secrets:/secrets + - '${PWD}/data/authelia/config:/config' + - '${PWD}/data/authelia/secrets:/secrets' networks: net: external: true - name: net + name: 'net' ``` ... {{< /details >}} diff --git a/docs/content/en/integration/ldap/introduction.md b/docs/content/en/integration/ldap/introduction.md index 6988ee9e7..2e9ebc4df 100644 --- a/docs/content/en/integration/ldap/introduction.md +++ b/docs/content/en/integration/ldap/introduction.md @@ -55,14 +55,16 @@ In your Authelia configuration you will need to enter and update the following v base_dn: DC=example,DC=com additional_users_dn: OU=users users_filter: (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)) - username_attribute: uid - mail_attribute: mail - display_name_attribute: displayName additional_groups_dn: OU=groups groups_filter: (&(member=UID={input},OU=users,DC=example,DC=com)(objectClass=groupOfNames)) - group_name_attribute: cn user: UID=authelia,OU=service accounts,DC=example,DC=com password: "SUPER_COMPLEX_PASSWORD" + attributes: + distinguished_name: 'distinguishedName' + username: 'uid' + mail: 'mail' + member_of: 'memberOf' + group_name: 'cn' ``` Following this, restart Authelia, and you should be able to begin using LDAP integration for your user logins, with Authelia taking the email attribute for users straight from the 'mail' attribute within the LDAP object. @@ -100,16 +102,18 @@ In your Authelia configuration you will need to enter and update the following v skip_verify: true minimum_version: TLS1.2 base_dn: dc=example,DC=com - username_attribute: uid additional_users_dn: CN=users,CN=accounts users_filter: (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)) additional_groups_dn: OU=groups groups_filter: (&(member=UID={input},CN=users,CN=accounts,DC=example,DC=com)(objectClass=groupOfNames)) - group_name_attribute: cn - mail_attribute: mail - display_name_attribute: displayName user: UID=authelia,CN=users,CN=accounts,DC=example,DC=com password: "SUPER_COMPLEX_PASSWORD" + attributes: + distinguished_name: 'distinguishedName' + username: 'uid' + mail: 'mail' + member_of: 'memberOf' + group_name: 'cn' ``` Following this, restart Authelia, and you should be able to begin using LDAP integration for your user logins, with Authelia taking the email attribute for users straight from the 'mail' attribute within the LDAP object. @@ -139,19 +143,21 @@ ldap: timeout: 5s start_tls: false base_dn: dc=example,DC=com - username_attribute: uid additional_users_dn: OU=people # To allow sign in both with username and email, one can use a filter like # (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)) users_filter: (&({username_attribute}={input})(objectClass=person)) additional_groups_dn: OU=groups groups_filter: (member={dn}) - group_name_attribute: cn - mail_attribute: mail - display_name_attribute: displayName # The username and password of the admin or service user. user: UID=authelia,OU=people,DC=example,DC=com password: "SUPER_COMPLEX_PASSWORD" + attributes: + distinguished_name: 'distinguishedName' + username: 'uid' + mail: 'mail' + member_of: 'memberOf' + group_name: 'cn' ``` Following this, restart Authelia, and you should be able to begin using lldap integration for your user logins, with Authelia taking the email attribute for users straight from the 'mail' attribute within the LDAP object. diff --git a/docs/content/en/reference/guides/ldap.md b/docs/content/en/reference/guides/ldap.md index 9f5626c27..397a06f61 100644 --- a/docs/content/en/reference/guides/ldap.md +++ b/docs/content/en/reference/guides/ldap.md @@ -75,6 +75,32 @@ The following implementations exist: [GLAuth]: https://glauth.github.io/ [RFC2307bis]: https://datatracker.ietf.org/doc/html/draft-howard-rfc2307bis-02 +### Group Search Modes + +There are currently two group search modes that exist. + +#### Search Mode: filter + +The `filter` search mode is the default search mode. Generally this is recommended. + +#### Search Mode: memberof + +The `memberof` search mode is a special search mode. Generally this is discouraged and is currently experimental. + +Some systems provide a `memberOf` attribute which may include additional groups that the user is a member of. This +search mode allows using this attribute as a method to determine their groups. How it works is the search is performed +against the base with the subtree scope and the groups filter must include one of the `{memberof:*}` replacements, and +the distinguished names of the results from the search are compared (case-insensitive) against the users `memberOf` +attribute to determine if they are members. + +This means: + +1. The groups still must be in the search base that you have configured. +2. The `memberOf` attribute *__MUST__* include the distinguished name of the group. +3. If the `{memberof:dn}` replacement is used: + 1. The distinguished name *__MUST__* be searchable by your directory server. +3. The first relative distinguished name of the distinguished name *__MUST__* be search + ### Filter replacements Various replacements occur in the user and groups filter. The replacements either occur at startup or upon an LDAP @@ -85,25 +111,65 @@ is ever established. In addition to this, during the startup phase we purposeful phase replacements exist so we only have to check if the replacement is necessary once, and we don't needlessly perform every possible replacement on every search regardless of if it's needed or not. +#### General filter replacements + +| Placeholder | Phase | Replacement | +|:------------------------------:|:-------:|:-------------------------------------------:| +| {distinguished_name_attribute} | startup | The configured distinguished name attribute | +| {username_attribute} | startup | The configured username attribute | +| {mail_attribute} | startup | The configured mail attribute | +| {display_name_attribute} | startup | The configured display name attribute | +| {member_of_attribute} | startup | The configured member of attribute | +| {input} | search | The input into the username field | + #### Users filter replacements -| Placeholder | Phase | Replacement | -|:------------------------:|:-------:|:----------------------------------------------------------------------------------------------------------------:| -| {username_attribute} | startup | The configured username attribute | -| {mail_attribute} | startup | The configured mail attribute | -| {display_name_attribute} | startup | The configured display name attribute | -| {input} | search | The input into the username field | -| {date-time:generalized} | search | The current UTC time formatted as a LDAP generalized time in the format of `20060102150405.0Z` | -| {date-time:unix} | search | The current time formatted as a Unix epoch | -| {date-time:microsoft-nt} | search | The current time formatted as a Microsoft NT epoch which is used by some Microsoft [Active Directory] attributes | +| Placeholder | Phase | Replacement | +|:------------------------------:|:-------:|:----------------------------------------------------------------------------------------------------------------:| +| {date-time:generalized} | search | The current UTC time formatted as a LDAP generalized time in the format of `20060102150405.0Z` | +| {date-time:unix} | search | The current time formatted as a Unix epoch | +| {date-time:microsoft-nt} | search | The current time formatted as a Microsoft NT epoch which is used by some Microsoft [Active Directory] attributes | #### Groups filter replacements -| Placeholder | Phase | Replacement | -|:-----------:|:------:|:-------------------------------------------------------------------------:| -| {input} | search | The input into the username field | -| {username} | search | The username from the profile lookup obtained from the username attribute | -| {dn} | search | The distinguished name from the profile lookup | +| Placeholder | Phase | Replacement | +|:--------------:|:------:|:----------------------------------------------------------------------------------------------------------------------------------------------------:| +| {username} | search | The username from the profile lookup obtained from the username attribute | +| {dn} | search | The distinguished name from the profile lookup | +| {memberof:dn} | search | See the detailed section below | +| {memberof:rdn} | search | Only allowed with the `memberof` search method and contains the first relative distinguished name of every `memberOf` entry a use has in parenthesis | + +##### memberof:dn + +Requirements: + +1. Must be using the `memberof` search mode. +2. Must have the distinguished name attribute configured in Authelia. +3. Directory server must support searching by the distinguished name attribute (many directory services *__DO NOT__* + have a distinguished name attribute). + +##### memberof:rdn + +Requirements: + +1. Must be using the `memberof` search mode. +2. Directory server must support searching by the first relative distinguished name as an attribute. + +Splits every `memberOf` value to obtain th e first relative distinguished name and joins all of those after surrounding +them in parenthesis. This makes the general suggested filter pattern for this particular replacement +`(|{memberof:rdn})`. The format of this value is as follows: + +```text +() +``` + +For example if the user has the following distinguished names in their object: + +- CN=abc,OU=groups,DC=example,DC=com +- CN=xyz,OU=groups,DC=example,DC=com + +The value will be replaced with `(CN=abc)(CN=xyz)` which using the suggested pattern for the filter becomes +`(|(CN=abc)(CN=xyz))` which will then return any user that as a `CN` of `abc` or `xyz`. ### Defaults @@ -122,14 +188,14 @@ The following set defaults for the `additional_users_dn` and `additional_groups_ This table describes the attribute defaults for each implementation. i.e. the username_attribute is described by the Username column. -| Implementation | Username | Display Name | Mail | Group Name | -|:---------------:|:--------------:|:------------:|:----:|:----------:| -| custom | N/A | displayName | mail | cn | -| activedirectory | sAMAccountName | displayName | mail | cn | -| rfc2307bis | uid | displayName | mail | cn | -| freeipa | uid | displayName | mail | cn | -| lldap | uid | cn | mail | cn | -| glauth | cn | description | mail | cn | +| Implementation | Username | Display Name | Mail | Group Name | Distinguished Name | Member Of | +|:---------------:|:--------------:|:------------:|:----:|:----------:|:------------------:|:---------:| +| custom | N/A | displayName | mail | cn | N/A | N/A | +| activedirectory | sAMAccountName | displayName | mail | cn | distinguishedName | memberOf | +| rfc2307bis | uid | displayName | mail | cn | N/A | memberOf | +| freeipa | uid | displayName | mail | cn | N/A | memberOf | +| lldap | uid | cn | mail | cn | N/A | memberOf | +| glauth | cn | description | mail | cn | N/A | memberOf | #### Filter defaults @@ -146,8 +212,8 @@ the following conditions: - Their password is expired: - The [Active Directory] implementation achieves this via the `(!(pwdLastSet=0))` filter. - The [FreeIPA] implementation achieves this via the `(krbPasswordExpiration>={date-time:generalized})` filter. + - The [RFC2307bis] implementation achieves this via the `(!(pwdReset=TRUE))` filter. - The following implementations have no suitable attribute for this as far as we're aware: - - [RFC2307bis] - [GLAuth] - [lldap] - Their account is expired: @@ -162,7 +228,7 @@ the following conditions: |:---------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-----------------------------------------------------------------------------------------------------------------------------------------:| | custom | N/A | N/A | | activedirectory | (&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:microsoft-nt}))) | (&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912))) | -| rfc2307bis | (&(|({username_attribute}={input})({mail_attribute}={input}))(|(objectClass=inetOrgPerson)(objectClass=organizationalPerson))) | (&(|(member={dn})(uniqueMember={dn}))(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)(objectClass=groupOfMembers))) | +| rfc2307bis | (&(|({username_attribute}={input})({mail_attribute}={input}))(|(objectClass=inetOrgPerson)(objectClass=organizationalPerson))(!(pwdReset=TRUE))) | (&(|(member={dn})(uniqueMember={dn}))(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)(objectClass=groupOfMembers))) | | freeipa | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))(krbPasswordExpiration>={date-time:generalized})(|(!(krbPrincipalExpiration=*))(krbPrincipalExpiration>={date-time:generalized}))) | (&(member={dn})(objectClass=groupOfNames)) | | lldap | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)) | (&(member={dn})(objectClass=groupOfNames)) | | glauth | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=posixAccount)(!(accountStatus=inactive))) | (&(uniqueMember={dn})(objectClass=posixGroup)) | diff --git a/docs/data/configkeys.json b/docs/data/configkeys.json index 751cc1767..a579aa18f 100644 --- a/docs/data/configkeys.json +++ b/docs/data/configkeys.json @@ -1 +1 @@ -[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.pushed_authorizations.enforce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_PUSHED_AUTHORIZATIONS_ENFORCE"},{"path":"identity_providers.oidc.pushed_authorizations.context_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_PUSHED_AUTHORIZATIONS_CONTEXT_LIFESPAN"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.watch","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_WATCH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.search.email","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_EMAIL"},{"path":"authentication_backend.file.search.case_insensitive","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_CASE_INSENSITIVE"},{"path":"authentication_backend.ldap.address","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDRESS"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.maximum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MAXIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.tls.private_key","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_PRIVATE_KEY_FILE"},{"path":"authentication_backend.ldap.tls.certificate_chain","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.username_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERNAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.mail_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_MAIL_ATTRIBUTE"},{"path":"authentication_backend.ldap.display_name_attribute","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_DISPLAY_NAME_ATTRIBUTE"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME"},{"path":"session","secret":false,"env":"AUTHELIA_SESSION"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.maximum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MAXIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.tls.private_key","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_PRIVATE_KEY_FILE"},{"path":"session.redis.tls.certificate_chain","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.address","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_ADDRESS"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MINIMUM_VERSION"},{"path":"storage.mysql.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MAXIMUM_VERSION"},{"path":"storage.mysql.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SKIP_VERIFY"},{"path":"storage.mysql.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SERVER_NAME"},{"path":"storage.mysql.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_PRIVATE_KEY_FILE"},{"path":"storage.mysql.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.address","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_ADDRESS"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MINIMUM_VERSION"},{"path":"storage.postgres.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MAXIMUM_VERSION"},{"path":"storage.postgres.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SKIP_VERIFY"},{"path":"storage.postgres.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SERVER_NAME"},{"path":"storage.postgres.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_PRIVATE_KEY_FILE"},{"path":"storage.postgres.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_ADDRESS"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.maximum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MAXIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.smtp.tls.private_key","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_PRIVATE_KEY_FILE"},{"path":"notifier.smtp.tls.certificate_chain","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.address","secret":false,"env":"AUTHELIA_SERVER_ADDRESS"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.endpoints.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_PPROF"},{"path":"server.endpoints.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_EXPVARS"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"},{"path":"privacy_policy.enabled","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_ENABLED"},{"path":"privacy_policy.require_user_acceptance","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_REQUIRE_USER_ACCEPTANCE"},{"path":"privacy_policy.policy_url","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_POLICY_URL"}] \ No newline at end of file +[{"path":"theme","secret":false,"env":"AUTHELIA_THEME"},{"path":"certificates_directory","secret":false,"env":"AUTHELIA_CERTIFICATES_DIRECTORY"},{"path":"jwt_secret","secret":true,"env":"AUTHELIA_JWT_SECRET_FILE"},{"path":"default_redirection_url","secret":false,"env":"AUTHELIA_DEFAULT_REDIRECTION_URL"},{"path":"default_2fa_method","secret":false,"env":"AUTHELIA_DEFAULT_2FA_METHOD"},{"path":"log.level","secret":false,"env":"AUTHELIA_LOG_LEVEL"},{"path":"log.format","secret":false,"env":"AUTHELIA_LOG_FORMAT"},{"path":"log.file_path","secret":false,"env":"AUTHELIA_LOG_FILE_PATH"},{"path":"log.keep_stdout","secret":false,"env":"AUTHELIA_LOG_KEEP_STDOUT"},{"path":"identity_providers.oidc.hmac_secret","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE"},{"path":"identity_providers.oidc.issuer_certificate_chain","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_CERTIFICATE_CHAIN_FILE"},{"path":"identity_providers.oidc.issuer_private_key","secret":true,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE"},{"path":"identity_providers.oidc.access_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ACCESS_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.authorize_code_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_AUTHORIZE_CODE_LIFESPAN"},{"path":"identity_providers.oidc.id_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ID_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.refresh_token_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_REFRESH_TOKEN_LIFESPAN"},{"path":"identity_providers.oidc.enable_client_debug_messages","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_CLIENT_DEBUG_MESSAGES"},{"path":"identity_providers.oidc.minimum_parameter_entropy","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_MINIMUM_PARAMETER_ENTROPY"},{"path":"identity_providers.oidc.enforce_pkce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENFORCE_PKCE"},{"path":"identity_providers.oidc.enable_pkce_plain_challenge","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_ENABLE_PKCE_PLAIN_CHALLENGE"},{"path":"identity_providers.oidc.pushed_authorizations.enforce","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_PUSHED_AUTHORIZATIONS_ENFORCE"},{"path":"identity_providers.oidc.pushed_authorizations.context_lifespan","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_PUSHED_AUTHORIZATIONS_CONTEXT_LIFESPAN"},{"path":"identity_providers.oidc.cors.allowed_origins_from_client_redirect_uris","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC_CORS_ALLOWED_ORIGINS_FROM_CLIENT_REDIRECT_URIS"},{"path":"identity_providers.oidc","secret":false,"env":"AUTHELIA_IDENTITY_PROVIDERS_OIDC"},{"path":"authentication_backend.password_reset.disable","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_DISABLE"},{"path":"authentication_backend.password_reset.custom_url","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_PASSWORD_RESET_CUSTOM_URL"},{"path":"authentication_backend.refresh_interval","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_REFRESH_INTERVAL"},{"path":"authentication_backend.file.path","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PATH"},{"path":"authentication_backend.file.watch","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_WATCH"},{"path":"authentication_backend.file.password.algorithm","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ALGORITHM"},{"path":"authentication_backend.file.password.argon2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_VARIANT"},{"path":"authentication_backend.file.password.argon2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_ITERATIONS"},{"path":"authentication_backend.file.password.argon2.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_MEMORY"},{"path":"authentication_backend.file.password.argon2.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_PARALLELISM"},{"path":"authentication_backend.file.password.argon2.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_KEY_LENGTH"},{"path":"authentication_backend.file.password.argon2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ARGON2_SALT_LENGTH"},{"path":"authentication_backend.file.password.sha2crypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_VARIANT"},{"path":"authentication_backend.file.password.sha2crypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.sha2crypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SHA2CRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.pbkdf2.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_VARIANT"},{"path":"authentication_backend.file.password.pbkdf2.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_ITERATIONS"},{"path":"authentication_backend.file.password.pbkdf2.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PBKDF2_SALT_LENGTH"},{"path":"authentication_backend.file.password.bcrypt.variant","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_VARIANT"},{"path":"authentication_backend.file.password.bcrypt.cost","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_BCRYPT_COST"},{"path":"authentication_backend.file.password.scrypt.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_ITERATIONS"},{"path":"authentication_backend.file.password.scrypt.block_size","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_BLOCK_SIZE"},{"path":"authentication_backend.file.password.scrypt.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_PARALLELISM"},{"path":"authentication_backend.file.password.scrypt.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_KEY_LENGTH"},{"path":"authentication_backend.file.password.scrypt.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SCRYPT_SALT_LENGTH"},{"path":"authentication_backend.file.password.iterations","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_ITERATIONS"},{"path":"authentication_backend.file.password.memory","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_MEMORY"},{"path":"authentication_backend.file.password.parallelism","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_PARALLELISM"},{"path":"authentication_backend.file.password.key_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_KEY_LENGTH"},{"path":"authentication_backend.file.password.salt_length","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_PASSWORD_SALT_LENGTH"},{"path":"authentication_backend.file.search.email","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_EMAIL"},{"path":"authentication_backend.file.search.case_insensitive","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_FILE_SEARCH_CASE_INSENSITIVE"},{"path":"authentication_backend.ldap.address","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDRESS"},{"path":"authentication_backend.ldap.implementation","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_IMPLEMENTATION"},{"path":"authentication_backend.ldap.timeout","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TIMEOUT"},{"path":"authentication_backend.ldap.start_tls","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_START_TLS"},{"path":"authentication_backend.ldap.tls.minimum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MINIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.maximum_version","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_MAXIMUM_VERSION"},{"path":"authentication_backend.ldap.tls.skip_verify","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SKIP_VERIFY"},{"path":"authentication_backend.ldap.tls.server_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_SERVER_NAME"},{"path":"authentication_backend.ldap.tls.private_key","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_PRIVATE_KEY_FILE"},{"path":"authentication_backend.ldap.tls.certificate_chain","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"authentication_backend.ldap.base_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_BASE_DN"},{"path":"authentication_backend.ldap.additional_users_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_USERS_DN"},{"path":"authentication_backend.ldap.users_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USERS_FILTER"},{"path":"authentication_backend.ldap.additional_groups_dn","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ADDITIONAL_GROUPS_DN"},{"path":"authentication_backend.ldap.groups_filter","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUPS_FILTER"},{"path":"authentication_backend.ldap.group_search_mode","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_GROUP_SEARCH_MODE"},{"path":"authentication_backend.ldap.attributes.distinguished_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ATTRIBUTES_DISTINGUISHED_NAME"},{"path":"authentication_backend.ldap.attributes.username","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ATTRIBUTES_USERNAME"},{"path":"authentication_backend.ldap.attributes.display_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ATTRIBUTES_DISPLAY_NAME"},{"path":"authentication_backend.ldap.attributes.mail","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ATTRIBUTES_MAIL"},{"path":"authentication_backend.ldap.attributes.member_of","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ATTRIBUTES_MEMBER_OF"},{"path":"authentication_backend.ldap.attributes.group_name","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_ATTRIBUTES_GROUP_NAME"},{"path":"authentication_backend.ldap.permit_referrals","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_REFERRALS"},{"path":"authentication_backend.ldap.permit_unauthenticated_bind","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_UNAUTHENTICATED_BIND"},{"path":"authentication_backend.ldap.permit_feature_detection_failure","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PERMIT_FEATURE_DETECTION_FAILURE"},{"path":"authentication_backend.ldap.user","secret":false,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_USER"},{"path":"authentication_backend.ldap.password","secret":true,"env":"AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE"},{"path":"session.secret","secret":true,"env":"AUTHELIA_SESSION_SECRET_FILE"},{"path":"session.name","secret":false,"env":"AUTHELIA_SESSION_NAME"},{"path":"session.domain","secret":false,"env":"AUTHELIA_SESSION_DOMAIN"},{"path":"session.same_site","secret":false,"env":"AUTHELIA_SESSION_SAME_SITE"},{"path":"session.expiration","secret":false,"env":"AUTHELIA_SESSION_EXPIRATION"},{"path":"session.inactivity","secret":false,"env":"AUTHELIA_SESSION_INACTIVITY"},{"path":"session.remember_me","secret":false,"env":"AUTHELIA_SESSION_REMEMBER_ME"},{"path":"session","secret":false,"env":"AUTHELIA_SESSION"},{"path":"session.redis.host","secret":false,"env":"AUTHELIA_SESSION_REDIS_HOST"},{"path":"session.redis.port","secret":false,"env":"AUTHELIA_SESSION_REDIS_PORT"},{"path":"session.redis.username","secret":false,"env":"AUTHELIA_SESSION_REDIS_USERNAME"},{"path":"session.redis.password","secret":true,"env":"AUTHELIA_SESSION_REDIS_PASSWORD_FILE"},{"path":"session.redis.database_index","secret":false,"env":"AUTHELIA_SESSION_REDIS_DATABASE_INDEX"},{"path":"session.redis.maximum_active_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MAXIMUM_ACTIVE_CONNECTIONS"},{"path":"session.redis.minimum_idle_connections","secret":false,"env":"AUTHELIA_SESSION_REDIS_MINIMUM_IDLE_CONNECTIONS"},{"path":"session.redis.tls.minimum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MINIMUM_VERSION"},{"path":"session.redis.tls.maximum_version","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_MAXIMUM_VERSION"},{"path":"session.redis.tls.skip_verify","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SKIP_VERIFY"},{"path":"session.redis.tls.server_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_TLS_SERVER_NAME"},{"path":"session.redis.tls.private_key","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_PRIVATE_KEY_FILE"},{"path":"session.redis.tls.certificate_chain","secret":true,"env":"AUTHELIA_SESSION_REDIS_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"session.redis.high_availability.sentinel_name","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_NAME"},{"path":"session.redis.high_availability.sentinel_username","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_USERNAME"},{"path":"session.redis.high_availability.sentinel_password","secret":true,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE"},{"path":"session.redis.high_availability.route_by_latency","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_BY_LATENCY"},{"path":"session.redis.high_availability.route_randomly","secret":false,"env":"AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_ROUTE_RANDOMLY"},{"path":"totp.disable","secret":false,"env":"AUTHELIA_TOTP_DISABLE"},{"path":"totp.issuer","secret":false,"env":"AUTHELIA_TOTP_ISSUER"},{"path":"totp.algorithm","secret":false,"env":"AUTHELIA_TOTP_ALGORITHM"},{"path":"totp.digits","secret":false,"env":"AUTHELIA_TOTP_DIGITS"},{"path":"totp.period","secret":false,"env":"AUTHELIA_TOTP_PERIOD"},{"path":"totp.skew","secret":false,"env":"AUTHELIA_TOTP_SKEW"},{"path":"totp.secret_size","secret":false,"env":"AUTHELIA_TOTP_SECRET_SIZE"},{"path":"duo_api.disable","secret":false,"env":"AUTHELIA_DUO_API_DISABLE"},{"path":"duo_api.hostname","secret":false,"env":"AUTHELIA_DUO_API_HOSTNAME"},{"path":"duo_api.integration_key","secret":true,"env":"AUTHELIA_DUO_API_INTEGRATION_KEY_FILE"},{"path":"duo_api.secret_key","secret":true,"env":"AUTHELIA_DUO_API_SECRET_KEY_FILE"},{"path":"duo_api.enable_self_enrollment","secret":false,"env":"AUTHELIA_DUO_API_ENABLE_SELF_ENROLLMENT"},{"path":"access_control.default_policy","secret":false,"env":"AUTHELIA_ACCESS_CONTROL_DEFAULT_POLICY"},{"path":"ntp.address","secret":false,"env":"AUTHELIA_NTP_ADDRESS"},{"path":"ntp.version","secret":false,"env":"AUTHELIA_NTP_VERSION"},{"path":"ntp.max_desync","secret":false,"env":"AUTHELIA_NTP_MAX_DESYNC"},{"path":"ntp.disable_startup_check","secret":false,"env":"AUTHELIA_NTP_DISABLE_STARTUP_CHECK"},{"path":"ntp.disable_failure","secret":false,"env":"AUTHELIA_NTP_DISABLE_FAILURE"},{"path":"regulation.max_retries","secret":false,"env":"AUTHELIA_REGULATION_MAX_RETRIES"},{"path":"regulation.find_time","secret":false,"env":"AUTHELIA_REGULATION_FIND_TIME"},{"path":"regulation.ban_time","secret":false,"env":"AUTHELIA_REGULATION_BAN_TIME"},{"path":"storage.local.path","secret":false,"env":"AUTHELIA_STORAGE_LOCAL_PATH"},{"path":"storage.mysql.address","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_ADDRESS"},{"path":"storage.mysql.database","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_DATABASE"},{"path":"storage.mysql.username","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_USERNAME"},{"path":"storage.mysql.password","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE"},{"path":"storage.mysql.timeout","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TIMEOUT"},{"path":"storage.mysql.host","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_HOST"},{"path":"storage.mysql.port","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_PORT"},{"path":"storage.mysql.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MINIMUM_VERSION"},{"path":"storage.mysql.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_MAXIMUM_VERSION"},{"path":"storage.mysql.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SKIP_VERIFY"},{"path":"storage.mysql.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_MYSQL_TLS_SERVER_NAME"},{"path":"storage.mysql.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_PRIVATE_KEY_FILE"},{"path":"storage.mysql.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_MYSQL_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.address","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_ADDRESS"},{"path":"storage.postgres.database","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_DATABASE"},{"path":"storage.postgres.username","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_USERNAME"},{"path":"storage.postgres.password","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE"},{"path":"storage.postgres.timeout","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TIMEOUT"},{"path":"storage.postgres.host","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_HOST"},{"path":"storage.postgres.port","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_PORT"},{"path":"storage.postgres.schema","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SCHEMA"},{"path":"storage.postgres.tls.minimum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MINIMUM_VERSION"},{"path":"storage.postgres.tls.maximum_version","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_MAXIMUM_VERSION"},{"path":"storage.postgres.tls.skip_verify","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SKIP_VERIFY"},{"path":"storage.postgres.tls.server_name","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_SERVER_NAME"},{"path":"storage.postgres.tls.private_key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_PRIVATE_KEY_FILE"},{"path":"storage.postgres.tls.certificate_chain","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"storage.postgres.ssl.mode","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_MODE"},{"path":"storage.postgres.ssl.root_certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_ROOT_CERTIFICATE"},{"path":"storage.postgres.ssl.certificate","secret":false,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_CERTIFICATE"},{"path":"storage.postgres.ssl.key","secret":true,"env":"AUTHELIA_STORAGE_POSTGRES_SSL_KEY_FILE"},{"path":"storage.encryption_key","secret":true,"env":"AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE"},{"path":"notifier.disable_startup_check","secret":false,"env":"AUTHELIA_NOTIFIER_DISABLE_STARTUP_CHECK"},{"path":"notifier.filesystem.filename","secret":false,"env":"AUTHELIA_NOTIFIER_FILESYSTEM_FILENAME"},{"path":"notifier.smtp.address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_ADDRESS"},{"path":"notifier.smtp.timeout","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TIMEOUT"},{"path":"notifier.smtp.username","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_USERNAME"},{"path":"notifier.smtp.password","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE"},{"path":"notifier.smtp.identifier","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_IDENTIFIER"},{"path":"notifier.smtp.sender","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SENDER"},{"path":"notifier.smtp.subject","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_SUBJECT"},{"path":"notifier.smtp.startup_check_address","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_STARTUP_CHECK_ADDRESS"},{"path":"notifier.smtp.disable_require_tls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_REQUIRE_TLS"},{"path":"notifier.smtp.disable_html_emails","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_HTML_EMAILS"},{"path":"notifier.smtp.disable_starttls","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_DISABLE_STARTTLS"},{"path":"notifier.smtp.tls.minimum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MINIMUM_VERSION"},{"path":"notifier.smtp.tls.maximum_version","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_MAXIMUM_VERSION"},{"path":"notifier.smtp.tls.skip_verify","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SKIP_VERIFY"},{"path":"notifier.smtp.tls.server_name","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_SERVER_NAME"},{"path":"notifier.smtp.tls.private_key","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_PRIVATE_KEY_FILE"},{"path":"notifier.smtp.tls.certificate_chain","secret":true,"env":"AUTHELIA_NOTIFIER_SMTP_TLS_CERTIFICATE_CHAIN_FILE"},{"path":"notifier.smtp.host","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_HOST"},{"path":"notifier.smtp.port","secret":false,"env":"AUTHELIA_NOTIFIER_SMTP_PORT"},{"path":"notifier.template_path","secret":false,"env":"AUTHELIA_NOTIFIER_TEMPLATE_PATH"},{"path":"server.address","secret":false,"env":"AUTHELIA_SERVER_ADDRESS"},{"path":"server.asset_path","secret":false,"env":"AUTHELIA_SERVER_ASSET_PATH"},{"path":"server.disable_healthcheck","secret":false,"env":"AUTHELIA_SERVER_DISABLE_HEALTHCHECK"},{"path":"server.tls.certificate","secret":false,"env":"AUTHELIA_SERVER_TLS_CERTIFICATE"},{"path":"server.tls.key","secret":true,"env":"AUTHELIA_SERVER_TLS_KEY_FILE"},{"path":"server.headers.csp_template","secret":false,"env":"AUTHELIA_SERVER_HEADERS_CSP_TEMPLATE"},{"path":"server.endpoints.enable_pprof","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_PPROF"},{"path":"server.endpoints.enable_expvars","secret":false,"env":"AUTHELIA_SERVER_ENDPOINTS_ENABLE_EXPVARS"},{"path":"server.buffers.read","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_READ"},{"path":"server.buffers.write","secret":false,"env":"AUTHELIA_SERVER_BUFFERS_WRITE"},{"path":"server.timeouts.read","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_READ"},{"path":"server.timeouts.write","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_WRITE"},{"path":"server.timeouts.idle","secret":false,"env":"AUTHELIA_SERVER_TIMEOUTS_IDLE"},{"path":"server.host","secret":false,"env":"AUTHELIA_SERVER_HOST"},{"path":"server.port","secret":false,"env":"AUTHELIA_SERVER_PORT"},{"path":"server.path","secret":false,"env":"AUTHELIA_SERVER_PATH"},{"path":"telemetry.metrics.enabled","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ENABLED"},{"path":"telemetry.metrics.address","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_ADDRESS"},{"path":"telemetry.metrics.buffers.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_READ"},{"path":"telemetry.metrics.buffers.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_BUFFERS_WRITE"},{"path":"telemetry.metrics.timeouts.read","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_READ"},{"path":"telemetry.metrics.timeouts.write","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_WRITE"},{"path":"telemetry.metrics.timeouts.idle","secret":false,"env":"AUTHELIA_TELEMETRY_METRICS_TIMEOUTS_IDLE"},{"path":"webauthn.disable","secret":false,"env":"AUTHELIA_WEBAUTHN_DISABLE"},{"path":"webauthn.display_name","secret":false,"env":"AUTHELIA_WEBAUTHN_DISPLAY_NAME"},{"path":"webauthn.attestation_conveyance_preference","secret":false,"env":"AUTHELIA_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE"},{"path":"webauthn.user_verification","secret":false,"env":"AUTHELIA_WEBAUTHN_USER_VERIFICATION"},{"path":"webauthn.timeout","secret":false,"env":"AUTHELIA_WEBAUTHN_TIMEOUT"},{"path":"password_policy.standard.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_ENABLED"},{"path":"password_policy.standard.min_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MIN_LENGTH"},{"path":"password_policy.standard.max_length","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_MAX_LENGTH"},{"path":"password_policy.standard.require_uppercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_UPPERCASE"},{"path":"password_policy.standard.require_lowercase","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_LOWERCASE"},{"path":"password_policy.standard.require_number","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_NUMBER"},{"path":"password_policy.standard.require_special","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_STANDARD_REQUIRE_SPECIAL"},{"path":"password_policy.zxcvbn.enabled","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_ENABLED"},{"path":"password_policy.zxcvbn.min_score","secret":false,"env":"AUTHELIA_PASSWORD_POLICY_ZXCVBN_MIN_SCORE"},{"path":"privacy_policy.enabled","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_ENABLED"},{"path":"privacy_policy.require_user_acceptance","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_REQUIRE_USER_ACCEPTANCE"},{"path":"privacy_policy.policy_url","secret":false,"env":"AUTHELIA_PRIVACY_POLICY_POLICY_URL"}] \ No newline at end of file diff --git a/internal/authentication/const.go b/internal/authentication/const.go index 51dc84db5..e63cf7edc 100644 --- a/internal/authentication/const.go +++ b/internal/authentication/const.go @@ -60,12 +60,19 @@ const ( ) const ( - ldapPlaceholderInput = "{input}" - ldapPlaceholderDistinguishedName = "{dn}" - ldapPlaceholderUsername = "{username}" - ldapPlaceholderDateTimeGeneralized = "{date-time:generalized}" - ldapPlaceholderDateTimeMicrosoftNTTimeEpoch = "{date-time:microsoft-nt}" - ldapPlaceholderDateTimeUnixEpoch = "{date-time:unix}" + ldapPlaceholderInput = "{input}" + ldapPlaceholderDistinguishedName = "{dn}" + ldapPlaceholderMemberOfDistinguishedName = "{memberof:dn}" + ldapPlaceholderMemberOfRelativeDistinguishedName = "{memberof:rdn}" + ldapPlaceholderUsername = "{username}" + ldapPlaceholderDateTimeGeneralized = "{date-time:generalized}" + ldapPlaceholderDateTimeMicrosoftNTTimeEpoch = "{date-time:microsoft-nt}" + ldapPlaceholderDateTimeUnixEpoch = "{date-time:unix}" + ldapPlaceholderDistinguishedNameAttribute = "{distinguished_name_attribute}" + ldapPlaceholderUsernameAttribute = "{username_attribute}" + ldapPlaceholderDisplayNameAttribute = "{display_name_attribute}" + ldapPlaceholderMailAttribute = "{mail_attribute}" + ldapPlaceholderMemberOfAttribute = "{member_of_attribute}" ) const ( diff --git a/internal/authentication/gen.go b/internal/authentication/gen.go index 8fe2c052d..d4fd84243 100644 --- a/internal/authentication/gen.go +++ b/internal/authentication/gen.go @@ -3,7 +3,7 @@ package authentication // This file is used to generate mocks. You can generate all mocks using the // command `go generate github.com/authelia/authelia/v4/internal/authentication`. -//go:generate mockgen -package authentication -destination ldap_client_mock.go -mock_names LDAPClient=MockLDAPClient github.com/authelia/authelia/v4/internal/authentication LDAPClient -//go:generate mockgen -package authentication -destination ldap_client_factory_mock.go -mock_names LDAPClientFactory=MockLDAPClientFactory github.com/authelia/authelia/v4/internal/authentication LDAPClientFactory +//go:generate mockgen -package authentication -destination ldap_client_mock_test.go -mock_names LDAPClient=MockLDAPClient github.com/authelia/authelia/v4/internal/authentication LDAPClient +//go:generate mockgen -package authentication -destination ldap_client_factory_mock_test.go -mock_names LDAPClientFactory=MockLDAPClientFactory github.com/authelia/authelia/v4/internal/authentication LDAPClientFactory //go:generate mockgen -package authentication -destination file_user_provider_database_mock_test.go -mock_names FileUserDatabase=MockFileUserDatabase github.com/authelia/authelia/v4/internal/authentication FileUserDatabase //go:generate mockgen -package authentication -destination file_user_provider_hash_mock_test.go -mock_names Hash=MockHash github.com/go-crypt/crypt/algorithm Hash diff --git a/internal/authentication/ldap_client_factory_mock.go b/internal/authentication/ldap_client_factory_mock_test.go similarity index 100% rename from internal/authentication/ldap_client_factory_mock.go rename to internal/authentication/ldap_client_factory_mock_test.go diff --git a/internal/authentication/ldap_client_mock.go b/internal/authentication/ldap_client_mock_test.go similarity index 100% rename from internal/authentication/ldap_client_mock.go rename to internal/authentication/ldap_client_mock_test.go diff --git a/internal/authentication/ldap_control_types_test.go b/internal/authentication/ldap_control_types_test.go new file mode 100644 index 000000000..dcbeb6dfd --- /dev/null +++ b/internal/authentication/ldap_control_types_test.go @@ -0,0 +1,17 @@ +package authentication + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestControlMsftServerPolicyHints(t *testing.T) { + ct := &controlMsftServerPolicyHints{ + oid: ldapOIDControlMsftServerPolicyHints, + } + + assert.Equal(t, ldapOIDControlMsftServerPolicyHints, ct.GetControlType()) + assert.Equal(t, "Enforce the password history length constraint (MS-SAMR section 3.1.1.7.1) during password set: 1.2.840.113556.1.4.2239", ct.String()) + assert.NotNil(t, ct.Encode()) +} diff --git a/internal/authentication/ldap_user_provider.go b/internal/authentication/ldap_user_provider.go index 5596872c0..df8f36da1 100644 --- a/internal/authentication/ldap_user_provider.go +++ b/internal/authentication/ldap_user_provider.go @@ -8,7 +8,7 @@ import ( "strconv" "strings" - "github.com/go-ldap/ldap/v3" + ldap "github.com/go-ldap/ldap/v3" "github.com/sirupsen/logrus" "github.com/authelia/authelia/v4/internal/configuration/schema" @@ -40,11 +40,13 @@ type LDAPUserProvider struct { usersFilterReplacementDateTimeMicrosoftNTTimeEpoch bool // Dynamically generated groups values. - groupsBaseDN string - groupsAttributes []string - groupsFilterReplacementInput bool - groupsFilterReplacementUsername bool - groupsFilterReplacementDN bool + groupsBaseDN string + groupsAttributes []string + groupsFilterReplacementInput bool + groupsFilterReplacementUsername bool + groupsFilterReplacementDN bool + groupsFilterReplacementsMemberOfDN bool + groupsFilterReplacementsMemberOfRDN bool } // NewLDAPUserProvider creates a new instance of LDAPUserProvider with the ProductionLDAPClientFactory. @@ -86,6 +88,7 @@ func NewLDAPUserProviderWithFactory(config schema.LDAPAuthenticationBackend, dis provider.parseDynamicUsersConfiguration() provider.parseDynamicGroupsConfiguration() + provider.parseDynamicConfiguration() return provider } @@ -134,38 +137,11 @@ func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, er } var ( - request *ldap.SearchRequest - result *ldap.SearchResult + groups []string ) - // Search for the users groups. - request = ldap.NewSearchRequest( - p.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, - 0, 0, false, p.resolveGroupsFilter(username, profile), p.groupsAttributes, nil, - ) - - p.log. - WithField("base_dn", request.BaseDN). - WithField("filter", request.Filter). - WithField("attr", request.Attributes). - WithField("scope", request.Scope). - WithField("deref", request.DerefAliases). - Trace("Performing group search") - - if result, err = p.search(client, request); err != nil { - return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", username, err) - } - - groups := make([]string, 0) - - for _, res := range result.Entries { - if len(res.Attributes) == 0 { - p.log.Warningf("No groups retrieved from LDAP for user %s", username) - break - } - - // Append all values of the document. Normally there should be only one per document. - groups = append(groups, res.Attributes[0].Values...) + if groups, err = p.getUserGroups(client, username, profile); err != nil { + return nil, err } return &UserDetails{ @@ -275,14 +251,12 @@ func (p *LDAPUserProvider) search(client LDAPClient, request *ldap.SearchRequest } else { result.Referrals = append(result.Referrals, referral) } + } else { + return nil, err } } if !p.config.PermitReferrals || len(result.Referrals) == 0 { - if err != nil { - return nil, err - } - return result, nil } @@ -357,6 +331,11 @@ func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string) (p return nil, fmt.Errorf("there were %d users found when searching for '%s' but there should only be 1", len(result.Entries), username) } + return p.getUserProfileResultToProfile(username, result) +} + +//nolint:gocyclo // Not overly complex. +func (p *LDAPUserProvider) getUserProfileResultToProfile(username string, result *ldap.SearchResult) (profile *ldapUserProfile, err error) { userProfile := ldapUserProfile{ DN: result.Entries[0].DN, } @@ -364,35 +343,50 @@ func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string) (p for _, attr := range result.Entries[0].Attributes { attrs := len(attr.Values) - if attr.Name == p.config.UsernameAttribute { + switch attr.Name { + case p.config.Attributes.Username: switch attrs { case 1: userProfile.Username = attr.Values[0] + + if attr.Name == p.config.Attributes.DisplayName && userProfile.DisplayName == "" { + userProfile.DisplayName = attr.Values[0] + } + + if attr.Name == p.config.Attributes.Mail && len(userProfile.Emails) == 0 { + userProfile.Emails = []string{attr.Values[0]} + } case 0: return nil, fmt.Errorf("user '%s' must have value for attribute '%s'", - username, p.config.UsernameAttribute) + username, p.config.Attributes.Username) default: return nil, fmt.Errorf("user '%s' has %d values for for attribute '%s' but the attribute must be a single value attribute", - username, attrs, p.config.UsernameAttribute) + username, attrs, p.config.Attributes.Username) + } + case p.config.Attributes.Mail: + if attrs == 0 { + continue } - } - if attrs == 0 { - continue - } - - if attr.Name == p.config.MailAttribute { userProfile.Emails = attr.Values - } + case p.config.Attributes.DisplayName: + if attrs == 0 { + continue + } - if attr.Name == p.config.DisplayNameAttribute { userProfile.DisplayName = attr.Values[0] + case p.config.Attributes.MemberOf: + if attrs == 0 { + continue + } + + userProfile.MemberOf = attr.Values } } if userProfile.Username == "" { return nil, fmt.Errorf("user '%s' must have value for attribute '%s'", - username, p.config.UsernameAttribute) + username, p.config.Attributes.Username) } if userProfile.DN == "" { @@ -402,6 +396,118 @@ func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string) (p return &userProfile, nil } +func (p *LDAPUserProvider) getUserGroups(client LDAPClient, username string, profile *ldapUserProfile) (groups []string, err error) { + request := ldap.NewSearchRequest( + p.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + 0, 0, false, p.resolveGroupsFilter(username, profile), p.groupsAttributes, nil, + ) + + p.log. + WithField("base_dn", request.BaseDN). + WithField("filter", request.Filter). + WithField("attributes", request.Attributes). + WithField("scope", request.Scope). + WithField("deref", request.DerefAliases). + WithField("mode", p.config.GroupSearchMode). + Trace("Performing group search") + + switch p.config.GroupSearchMode { + case "", "filter": + return p.getUserGroupsRequestFilter(client, username, profile, request) + case "memberof": + return p.getUserGroupsRequestMemberOf(client, username, profile, request) + default: + return nil, fmt.Errorf("could not perform group search with mode '%s' as it's unknown", p.config.GroupSearchMode) + } +} + +func (p *LDAPUserProvider) getUserGroupsRequestFilter(client LDAPClient, username string, _ *ldapUserProfile, request *ldap.SearchRequest) (groups []string, err error) { + var result *ldap.SearchResult + + if result, err = p.search(client, request); err != nil { + return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", username, err) + } + + for _, entry := range result.Entries { + if group := p.getUserGroupFromEntry(entry); len(group) != 0 { + groups = append(groups, group) + } + } + + return groups, nil +} + +func (p *LDAPUserProvider) getUserGroupsRequestMemberOf(client LDAPClient, username string, profile *ldapUserProfile, request *ldap.SearchRequest) (groups []string, err error) { + var result *ldap.SearchResult + + if result, err = p.search(client, request); err != nil { + return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", username, err) + } + + for _, entry := range result.Entries { + if len(entry.Attributes) == 0 { + p.log. + WithField("dn", entry.DN). + WithField("attributes", request.Attributes). + WithField("mode", "memberof"). + Trace("Skipping Group as the server did not return any requested attributes") + + continue + } + + if !utils.IsStringInSliceFold(entry.DN, profile.MemberOf) { + p.log. + WithField("dn", entry.DN). + WithField("mode", "memberof"). + Trace("Skipping Group as it doesn't match the users memberof entries") + + continue + } + + if group := p.getUserGroupFromEntry(entry); len(group) != 0 { + groups = append(groups, group) + } + } + + return groups, nil +} + +func (p *LDAPUserProvider) getUserGroupFromEntry(entry *ldap.Entry) string { +attributes: + for _, attr := range entry.Attributes { + switch attr.Name { + case p.config.Attributes.GroupName: + switch len(attr.Values) { + case 0: + p.log. + WithField("dn", entry.DN). + WithField("attribute", attr.Name). + Trace("Group skipped as the server returned a null attribute") + case 1: + switch len(attr.Values[0]) { + case 0: + p.log. + WithField("dn", entry.DN). + WithField("attribute", attr.Name). + Trace("Skipping group as the configured group name attribute had no value") + + default: + return attr.Values[0] + } + default: + p.log. + WithField("dn", entry.DN). + WithField("attribute", attr.Name). + Trace("Group skipped as the server returned a multi-valued attribute but it should be a single-valued attribute") + } + + break attributes + } + } + + return "" +} + func (p *LDAPUserProvider) resolveUsersFilter(input string) (filter string) { filter = p.config.UsersFilter @@ -445,6 +551,27 @@ func (p *LDAPUserProvider) resolveGroupsFilter(input string, profile *ldapUserPr } } + if p.groupsFilterReplacementsMemberOfDN { + sep := fmt.Sprintf(")(%s=", p.config.Attributes.DistinguishedName) + values := make([]string, len(profile.MemberOf)) + + for i, memberof := range profile.MemberOf { + values[i] = ldap.EscapeFilter(memberof) + } + + filter = strings.ReplaceAll(filter, ldapPlaceholderMemberOfDistinguishedName, fmt.Sprintf("(%s=%s)", p.config.Attributes.DistinguishedName, strings.Join(values, sep))) + } + + if p.groupsFilterReplacementsMemberOfRDN { + values := make([]string, len(profile.MemberOf)) + + for i, memberof := range profile.MemberOf { + values[i] = ldap.EscapeFilter(strings.SplitN(memberof, ",", 2)[0]) + } + + filter = strings.ReplaceAll(filter, ldapPlaceholderMemberOfRelativeDistinguishedName, fmt.Sprintf("(%s)", strings.Join(values, ")("))) + } + p.log.Tracef("Computed groups filter is %s", filter) return filter diff --git a/internal/authentication/ldap_user_provider_startup.go b/internal/authentication/ldap_user_provider_startup.go index 076b02fd4..060f3a50b 100644 --- a/internal/authentication/ldap_user_provider_startup.go +++ b/internal/authentication/ldap_user_provider_startup.go @@ -37,11 +37,6 @@ func (p *LDAPUserProvider) StartupCheck() (err error) { "LDAP Server.") } - if !p.features.Extensions.TLS && p.config.StartTLS { - p.log.Info("Your LDAP Server does not appear to support TLS but you enabled StartTLS which may result " + - "in an error.") - } - return nil } @@ -90,22 +85,24 @@ func (p *LDAPUserProvider) getServerSupportedFeatures(client LDAPClient) (featur } func (p *LDAPUserProvider) parseDynamicUsersConfiguration() { - p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, "{username_attribute}", p.config.UsernameAttribute) - p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, "{mail_attribute}", p.config.MailAttribute) - p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, "{display_name_attribute}", p.config.DisplayNameAttribute) + p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderDistinguishedNameAttribute, p.config.Attributes.DistinguishedName) + p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderUsernameAttribute, p.config.Attributes.Username) + p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderDisplayNameAttribute, p.config.Attributes.DisplayName) + p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderMailAttribute, p.config.Attributes.Mail) + p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, ldapPlaceholderMemberOfAttribute, p.config.Attributes.MemberOf) p.log.Tracef("Dynamically generated users filter is %s", p.config.UsersFilter) - if !utils.IsStringInSlice(p.config.UsernameAttribute, p.usersAttributes) { - p.usersAttributes = append(p.usersAttributes, p.config.UsernameAttribute) + if len(p.config.Attributes.Username) != 0 && !utils.IsStringInSlice(p.config.Attributes.Username, p.usersAttributes) { + p.usersAttributes = append(p.usersAttributes, p.config.Attributes.Username) } - if !utils.IsStringInSlice(p.config.MailAttribute, p.usersAttributes) { - p.usersAttributes = append(p.usersAttributes, p.config.MailAttribute) + if len(p.config.Attributes.Mail) != 0 && !utils.IsStringInSlice(p.config.Attributes.Mail, p.usersAttributes) { + p.usersAttributes = append(p.usersAttributes, p.config.Attributes.Mail) } - if !utils.IsStringInSlice(p.config.DisplayNameAttribute, p.usersAttributes) { - p.usersAttributes = append(p.usersAttributes, p.config.DisplayNameAttribute) + if len(p.config.Attributes.DisplayName) != 0 && !utils.IsStringInSlice(p.config.Attributes.DisplayName, p.usersAttributes) { + p.usersAttributes = append(p.usersAttributes, p.config.Attributes.DisplayName) } if p.config.AdditionalUsersDN != "" { @@ -137,8 +134,14 @@ func (p *LDAPUserProvider) parseDynamicUsersConfiguration() { } func (p *LDAPUserProvider) parseDynamicGroupsConfiguration() { - p.groupsAttributes = []string{ - p.config.GroupNameAttribute, + p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderDistinguishedNameAttribute, p.config.Attributes.DistinguishedName) + p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderUsernameAttribute, p.config.Attributes.Username) + p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderDisplayNameAttribute, p.config.Attributes.DisplayName) + p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderMailAttribute, p.config.Attributes.Mail) + p.config.GroupsFilter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderMemberOfAttribute, p.config.Attributes.MemberOf) + + if len(p.config.Attributes.GroupName) != 0 && !utils.IsStringInSlice(p.config.Attributes.GroupName, p.groupsAttributes) { + p.groupsAttributes = append(p.groupsAttributes, p.config.Attributes.GroupName) } if p.config.AdditionalGroupsDN != "" { @@ -161,5 +164,25 @@ func (p *LDAPUserProvider) parseDynamicGroupsConfiguration() { p.groupsFilterReplacementDN = true } + if strings.Contains(p.config.GroupsFilter, ldapPlaceholderMemberOfDistinguishedName) { + p.groupsFilterReplacementsMemberOfDN = true + } + + if strings.Contains(p.config.GroupsFilter, ldapPlaceholderMemberOfRelativeDistinguishedName) { + p.groupsFilterReplacementsMemberOfRDN = true + } + p.log.Tracef("Detected group filter replacements that need to be resolved per lookup are: input=%v, username=%v, dn=%v", p.groupsFilterReplacementInput, p.groupsFilterReplacementUsername, p.groupsFilterReplacementDN) } + +func (p *LDAPUserProvider) parseDynamicConfiguration() { + if len(p.config.Attributes.MemberOf) != 0 { + if !utils.IsStringInSlice(p.config.Attributes.MemberOf, p.usersAttributes) { + p.usersAttributes = append(p.usersAttributes, p.config.Attributes.MemberOf) + } + + if !utils.IsStringInSlice(p.config.Attributes.MemberOf, p.groupsAttributes) { + p.groupsAttributes = append(p.groupsAttributes, p.config.Attributes.MemberOf) + } + } +} diff --git a/internal/authentication/ldap_user_provider_test.go b/internal/authentication/ldap_user_provider_test.go index 7dc7c7e20..1664909fa 100644 --- a/internal/authentication/ldap_user_provider_test.go +++ b/internal/authentication/ldap_user_provider_test.go @@ -16,6 +16,20 @@ import ( "github.com/authelia/authelia/v4/internal/utils" ) +func TestNewLDAPUserProvider(t *testing.T) { + provider := NewLDAPUserProvider(schema.AuthenticationBackend{LDAP: &schema.LDAPAuthenticationBackend{}}, nil) + + assert.NotNil(t, provider) +} + +func TestNewLDAPUserProviderWithFactoryWithoutFactory(t *testing.T) { + provider := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackend{}, false, nil, nil) + + assert.NotNil(t, provider) + + assert.IsType(t, &ProductionLDAPClientFactory{}, provider.factory) +} + func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -128,6 +142,79 @@ func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) { assert.Equal(t, "(|(member=cn=john \\28external\\29,dc=example,dc=com)(uid=john)(uid=john\\#\\=\\28abc\\,def\\29))", filter) } +func TestResolveGroupsFilter(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + + testCases := []struct { + name string + have schema.LDAPAuthenticationBackend + input string + profile ldapUserProfile + expected string + }{ + { + "ShouldResolveEmptyFilter", + schema.LDAPAuthenticationBackend{}, + "", + ldapUserProfile{}, + "", + }, + { + "ShouldResolveMemberOfRDNFilter", + schema.LDAPAuthenticationBackend{ + GroupsFilter: "(|{memberof:rdn})", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + GroupName: "cn", + MemberOf: "memberOf", + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + }, + }, + "", + ldapUserProfile{ + MemberOf: []string{"CN=abc,DC=example,DC=com", "CN=xyz,DC=example,DC=com"}, + }, + "(|(CN=abc)(CN=xyz))", + }, + { + "ShouldResolveMemberOfDNFilter", + schema.LDAPAuthenticationBackend{ + GroupsFilter: "(|{memberof:dn})", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + GroupName: "cn", + MemberOf: "memberOf", + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + }, + }, + "", + ldapUserProfile{ + MemberOf: []string{"CN=abc,DC=example,DC=com", "CN=xyz,DC=example,DC=com"}, + }, + "(|(distinguishedName=CN=abc,DC=example,DC=com)(distinguishedName=CN=xyz,DC=example,DC=com))", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provider := NewLDAPUserProviderWithFactory( + tc.have, + false, + nil, + mockFactory) + + assert.Equal(t, tc.expected, provider.resolveGroupsFilter("", &tc.profile)) + }) + } +} + type ExtendedSearchRequestMatcher struct { filter string baseDN string @@ -156,6 +243,81 @@ func (e *ExtendedSearchRequestMatcher) String() string { return fmt.Sprintf("baseDN: %s, filter %s", e.baseDN, e.filter) } +func TestShouldCheckLDAPEpochFilters(t *testing.T) { + type have struct { + users string + attr schema.LDAPAuthenticationAttributes + } + + type expected struct { + dtgeneralized bool + dtmsftnt bool + dtunix bool + } + + testCases := []struct { + name string + have have + expected expected + }{ + { + "ShouldNotEnableAny", + have{}, + expected{}, + }, + { + "ShouldNotEnableMSFTNT", + have{ + users: "(abc={date-time:microsoft-nt})", + }, + expected{ + dtmsftnt: true, + }, + }, + { + "ShouldNotEnableUnix", + have{ + users: "(abc={date-time:unix})", + }, + expected{ + dtunix: true, + }, + }, + { + "ShouldNotEnableGeneralized", + have{ + users: "(abc={date-time:generalized})", + }, + expected{ + dtgeneralized: true, + }, + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + UsersFilter: tc.have.users, + Attributes: tc.have.attr, + BaseDN: "dc=example,dc=com", + }, + false, + nil, + mockFactory) + + assert.Equal(t, tc.expected.dtgeneralized, provider.usersFilterReplacementDateTimeGeneralized) + assert.Equal(t, tc.expected.dtmsftnt, provider.usersFilterReplacementDateTimeMicrosoftNTTimeEpoch) + assert.Equal(t, tc.expected.dtunix, provider.usersFilterReplacementDateTimeUnixEpoch) + }) + } +} + func TestShouldCheckLDAPServerExtensions(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -165,15 +327,18 @@ func TestShouldCheckLDAPServerExtensions(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -230,15 +395,18 @@ func TestShouldNotCheckLDAPServerExtensionsWhenRootDSEReturnsMoreThanOneEntry(t provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -296,15 +464,18 @@ func TestShouldCheckLDAPServerControlTypes(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -361,15 +532,18 @@ func TestShouldNotEnablePasswdModifyExtensionOrControlTypes(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -426,15 +600,18 @@ func TestShouldReturnCheckServerConnectError(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -459,15 +636,18 @@ func TestShouldReturnCheckServerSearchError(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -495,6 +675,53 @@ func TestShouldReturnCheckServerSearchError(t *testing.T) { assert.False(t, provider.features.Extensions.PwdModifyExOp) } +func TestShouldPermitRootDSEFailure(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + PermitFeatureDetectionFailure: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(nil, errors.New("could not perform the search")) + + connClose := mockClient.EXPECT().Close() + + gomock.InOrder(dialURL, connBind, searchOIDs, connClose) + + err := provider.StartupCheck() + assert.NoError(t, err) +} + type SearchRequestMatcher struct { expected string } @@ -521,16 +748,19 @@ func TestShouldEscapeUserInput(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -555,21 +785,24 @@ func TestShouldReturnEmailWhenAttributeSameAsUsername(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "mail", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "(&({username_attribute}={input})(objectClass=inetOrgPerson))", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "mail", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "(&({username_attribute}={input})(objectClass=inetOrgPerson))", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, mockFactory) - assert.Equal(t, []string{"mail", "displayName"}, provider.usersAttributes) + assert.Equal(t, []string{"mail", "displayName", "memberOf"}, provider.usersAttributes) dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). @@ -626,21 +859,24 @@ func TestShouldReturnUsernameAndBlankDisplayNameWhenAttributesTheSame(t *testing provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "uid", - UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=inetOrgPerson))", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "uid", + MemberOf: "memberOf", + }, + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=inetOrgPerson))", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, mockFactory) - assert.Equal(t, []string{"uid", "mail"}, provider.usersAttributes) + assert.Equal(t, []string{"uid", "mail", "memberOf"}, provider.usersAttributes) dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). @@ -697,21 +933,24 @@ func TestShouldReturnBlankEmailAndDisplayNameWhenAttrsLenZero(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=inetOrgPerson))", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=inetOrgPerson))", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, mockFactory) - assert.Equal(t, []string{"uid", "mail", "displayName"}, provider.usersAttributes) + assert.Equal(t, []string{"uid", "mail", "displayName", "memberOf"}, provider.usersAttributes) dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). @@ -740,6 +979,10 @@ func TestShouldReturnBlankEmailAndDisplayNameWhenAttrsLenZero(t *testing.T) { Name: "displayName", Values: []string{}, }, + { + Name: "memberOf", + Values: []string{}, + }, }, }, }, @@ -771,22 +1014,25 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - UsernameAttribute: "uid", - UsersFilter: "(&({username_attribute}={input})(&(objectCategory=person)(objectClass=user)))", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(&({username_attribute}={input})(&(objectCategory=person)(objectClass=user)))", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, mockFactory) - assert.Equal(t, []string{"uid", "mail", "displayName"}, provider.usersAttributes) + assert.Equal(t, []string{"uid", "mail", "displayName", "memberOf"}, provider.usersAttributes) assert.True(t, provider.usersFilterReplacementInput) @@ -807,10 +1053,35 @@ func createSearchResultWithAttributes(attributes ...*ldap.EntryAttribute) *ldap. } } -func createSearchResultWithAttributeValues(values ...string) *ldap.SearchResult { - return createSearchResultWithAttributes(&ldap.EntryAttribute{ - Values: values, - }) +func createGroupSearchResultModeFilter(name string, groupNames ...string) *ldap.SearchResult { + result := &ldap.SearchResult{ + Entries: make([]*ldap.Entry, len(groupNames)), + } + + for i, groupName := range groupNames { + result.Entries[i] = &ldap.Entry{Attributes: []*ldap.EntryAttribute{{Name: name, Values: []string{groupName}}}} + } + + return result +} + +func createGroupSearchResultModeFilterWithDN(name string, groupNames []string, groupDNs []string) *ldap.SearchResult { + if len(groupNames) != len(groupDNs) { + panic("input sizes mismatch") + } + + result := &ldap.SearchResult{ + Entries: make([]*ldap.Entry, len(groupNames)), + } + + for i, groupName := range groupNames { + result.Entries[i] = &ldap.Entry{ + DN: groupDNs[i], + Attributes: []*ldap.EntryAttribute{{Name: name, Values: []string{groupName}}}, + } + } + + return result } func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { @@ -822,16 +1093,20 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -886,6 +1161,127 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { assert.Equal(t, details.Username, "john") } +func TestLDAPUserProvider_GetDetails_ShouldReturnOnUserError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(nil, fmt.Errorf("failed to search")) + + gomock.InOrder(dialURL, connBind, searchProfile, connClose) + + details, err := provider.GetDetails("john") + assert.Nil(t, details) + assert.EqualError(t, err, "cannot find user DN of user 'john'. Cause: failed to search") +} + +func TestLDAPUserProvider_GetDetails_ShouldReturnOnGroupsError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchGroups := mockClient.EXPECT(). + Search(gomock.Any()). + Return(nil, fmt.Errorf("failed to search groups")) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "uid", + Values: []string{"john"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, searchGroups, connClose) + + details, err := provider.GetDetails("john") + + assert.Nil(t, details) + assert.EqualError(t, err, "unable to retrieve groups of user 'john'. Cause: failed to search groups") +} + func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -895,10 +1291,15 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "displayName", + }, UsersFilter: "uid={input}", AdditionalUsersDN: "ou=users", BaseDN: "dc=example,dc=com", @@ -919,7 +1320,73 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { searchGroups := mockClient.EXPECT(). Search(gomock.Any()). - Return(createSearchResultWithAttributeValues("group1", "group2"), nil) + Return(createGroupSearchResultModeFilter(provider.config.Attributes.GroupName, "group1", "group2"), nil) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "uid", + Values: []string{"john"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, searchGroups, connClose) + + details, err := provider.GetDetails("john") + require.NoError(t, err) + + assert.ElementsMatch(t, details.Groups, []string{"group1", "group2"}) + assert.ElementsMatch(t, details.Emails, []string{}) + assert.Equal(t, details.Username, "john") +} + +func TestShouldUnauthenticatedBind(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "displayName", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + UnauthenticatedBind(gomock.Eq("cn=admin,dc=example,dc=com")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchGroups := mockClient.EXPECT(). + Search(gomock.Any()). + Return(createGroupSearchResultModeFilter(provider.config.Attributes.GroupName, "group1", "group2"), nil) searchProfile := mockClient.EXPECT(). Search(gomock.Any()). @@ -956,15 +1423,19 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -982,7 +1453,7 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) { searchGroups := mockClient.EXPECT(). Search(gomock.Any()). - Return(createSearchResultWithAttributeValues("group1", "group2"), nil) + Return(createGroupSearchResultModeFilter(provider.config.Attributes.GroupName, "group1", "group2"), nil) searchProfile := mockClient.EXPECT(). Search(gomock.Any()). @@ -1019,26 +1490,595 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) { assert.Equal(t, details.Username, "John") } -func TestShouldReturnUsernameFromLDAPWithReferrals(t *testing.T) { +func TestShouldReturnUsernameFromLDAPSearchModeMemberOfRDN(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockFactory := NewMockLDAPClientFactory(ctrl) mockClient := NewMockLDAPClient(ctrl) - mockClientReferral := NewMockLDAPClient(ctrl) provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + GroupSearchMode: "memberof", + UsersFilter: "uid={input}", + GroupsFilter: "(|{memberof:rdn})", + AdditionalUsersDN: "ou=users", + BaseDN: "DC=example,DC=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + requestGroups := ldap.NewSearchRequest( + provider.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + 0, 0, false, "(|(CN=admins)(CN=users))", provider.groupsAttributes, nil, + ) + + // This ensures the filtering works correctly in the following ways: + // Item 1 (0th element), has the wrong case. + // Item 2 (1st element), has the wrong DN. + searchGroups := mockClient.EXPECT(). + Search(requestGroups). + Return(createGroupSearchResultModeFilterWithDN(provider.config.Attributes.GroupName, []string{"admins", "notadmins", "users"}, []string{"CN=ADMINS,OU=groups,DC=example,DC=com", "CN=notadmins,OU=wronggroups,DC=example,DC=com", "CN=users,OU=groups,DC=example,DC=com"}), nil) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "uid", + Values: []string{"John"}, + }, + { + Name: "memberOf", + Values: []string{"CN=admins,OU=groups,DC=example,DC=com", "CN=users,OU=groups,DC=example,DC=com"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, searchGroups, connClose) + + details, err := provider.GetDetails("john") + require.NoError(t, err) + + assert.ElementsMatch(t, details.Groups, []string{"admins", "users"}) + assert.ElementsMatch(t, details.Emails, []string{"test@example.com"}) + assert.Equal(t, details.DisplayName, "John Doe") + assert.Equal(t, details.Username, "John") +} + +func TestShouldReturnUsernameFromLDAPSearchModeMemberOfDN(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "CN=Administrator,CN=Users,DC=example,DC=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + Username: "sAMAccountName", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + GroupSearchMode: "memberof", + UsersFilter: "sAMAccountName={input}", + GroupsFilter: "(|{memberof:dn})", + AdditionalUsersDN: "CN=users", + BaseDN: "DC=example,DC=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("CN=Administrator,CN=Users,DC=example,DC=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + requestGroups := ldap.NewSearchRequest( + provider.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + 0, 0, false, "(|(distinguishedName=CN=admins,OU=groups,DC=example,DC=com)(distinguishedName=CN=users,OU=groups,DC=example,DC=com))", provider.groupsAttributes, nil, + ) + + searchGroups := mockClient.EXPECT(). + Search(requestGroups). + Return(createGroupSearchResultModeFilterWithDN(provider.config.Attributes.GroupName, []string{"admins", "admins", "users"}, []string{"CN=admins,OU=groups,DC=example,DC=com", "CN=admins,OU=wronggroups,DC=example,DC=com", "CN=users,OU=groups,DC=example,DC=com"}), nil) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "sAMAccountName", + Values: []string{"John"}, + }, + { + Name: "memberOf", + Values: []string{"CN=admins,OU=groups,DC=example,DC=com", "CN=users,OU=groups,DC=example,DC=com"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, searchGroups, connClose) + + details, err := provider.GetDetails("john") + require.NoError(t, err) + + assert.ElementsMatch(t, details.Groups, []string{"admins", "users"}) + assert.ElementsMatch(t, details.Emails, []string{"test@example.com"}) + assert.Equal(t, details.DisplayName, "John Doe") + assert.Equal(t, details.Username, "John") +} + +func TestShouldReturnErrSearchMemberOf(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "CN=Administrator,CN=Users,DC=example,DC=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + Username: "sAMAccountName", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + GroupSearchMode: "memberof", + UsersFilter: "sAMAccountName={input}", + GroupsFilter: "(|{memberof:dn})", + AdditionalUsersDN: "CN=users", + BaseDN: "DC=example,DC=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("CN=Administrator,CN=Users,DC=example,DC=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + requestGroups := ldap.NewSearchRequest( + provider.groupsBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + 0, 0, false, "(|(distinguishedName=CN=admins,OU=groups,DC=example,DC=com)(distinguishedName=CN=users,OU=groups,DC=example,DC=com))", provider.groupsAttributes, nil, + ) + + searchGroups := mockClient.EXPECT(). + Search(requestGroups). + Return(nil, fmt.Errorf("failed to get groups")) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "sAMAccountName", + Values: []string{"John"}, + }, + { + Name: "memberOf", + Values: []string{"CN=admins,OU=groups,DC=example,DC=com", "CN=users,OU=groups,DC=example,DC=com"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, searchGroups, connClose) + + details, err := provider.GetDetails("john") + assert.Nil(t, details) + assert.EqualError(t, err, "unable to retrieve groups of user 'john'. Cause: failed to get groups") +} + +func TestShouldReturnErrUnknownSearchMode(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "CN=Administrator,CN=Users,DC=example,DC=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + Username: "sAMAccountName", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + GroupSearchMode: "bad", + UsersFilter: "sAMAccountName={input}", + GroupsFilter: "(|{memberof:dn})", + AdditionalUsersDN: "CN=users", + BaseDN: "DC=example,DC=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("CN=Administrator,CN=Users,DC=example,DC=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "sAMAccountName", + Values: []string{"John"}, + }, + { + Name: "memberOf", + Values: []string{"CN=admins,OU=groups,DC=example,DC=com", "CN=users,OU=groups,DC=example,DC=com"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, connClose) + + details, err := provider.GetDetails("john") + assert.Nil(t, details) + + assert.EqualError(t, err, "could not perform group search with mode 'bad' as it's unknown") +} + +func TestShouldSkipEmptyAttributesSearchModeMemberOf(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "CN=Administrator,CN=Users,DC=example,DC=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + Username: "sAMAccountName", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + GroupSearchMode: "memberof", + UsersFilter: "sAMAccountName={input}", + GroupsFilter: "(|{memberof:dn})", + AdditionalUsersDN: "CN=users", + BaseDN: "DC=example,DC=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("CN=Administrator,CN=Users,DC=example,DC=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "sAMAccountName", + Values: []string{"John"}, + }, + { + Name: "memberOf", + Values: []string{"CN=admins,OU=groups,DC=example,DC=com", "CN=users,OU=groups,DC=example,DC=com", "CN=multi,OU=groups,DC=example,DC=com"}, + }, + }, + }, + }, + }, nil) + + searchGroups := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=grp,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{}, + }, + { + DN: "CN=users,OU=groups,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{}, + }, + }, + }, + { + DN: "CN=admins,OU=groups,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{""}, + }, + }, + }, + { + DN: "CN=multi,OU=groups,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{"a", "b"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, searchGroups, connClose) + + details, err := provider.GetDetails("john") + + assert.NoError(t, err) + assert.NotNil(t, details) +} + +func TestShouldSkipEmptyAttributesSearchModeFilter(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "CN=Administrator,CN=Users,DC=example,DC=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + Username: "sAMAccountName", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + GroupSearchMode: "filter", + UsersFilter: "sAMAccountName={input}", + GroupsFilter: "(|{memberof:dn})", + AdditionalUsersDN: "CN=users", + BaseDN: "DC=example,DC=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("CN=Administrator,CN=Users,DC=example,DC=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "sAMAccountName", + Values: []string{"John"}, + }, + { + Name: "memberOf", + Values: []string{"CN=admins,OU=groups,DC=example,DC=com", "CN=users,OU=groups,DC=example,DC=com", "CN=multi,OU=groups,DC=example,DC=com"}, + }, + }, + }, + }, + }, nil) + + searchGroups := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=grp,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{}, + }, + { + DN: "CN=users,OU=groups,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{}, + }, + }, + }, + { + DN: "CN=admins,OU=groups,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{""}, + }, + }, + }, + { + DN: "CN=multi,OU=groups,DC=example,DC=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "cn", + Values: []string{"a", "b"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, searchGroups, connClose) + + details, err := provider.GetDetails("john") + + assert.NoError(t, err) + assert.NotNil(t, details) +} + +func TestShouldSkipEmptyGroupsResultMemberOf(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -1055,27 +2095,14 @@ func TestShouldReturnUsernameFromLDAPWithReferrals(t *testing.T) { connClose := mockClient.EXPECT().Close() searchGroups := mockClient.EXPECT(). - Search(gomock.Any()). - Return(createSearchResultWithAttributeValues("group1", "group2"), nil) - - searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ - Entries: []*ldap.Entry{}, - Referrals: []string{"ldap://192.168.2.1"}, + Entries: []*ldap.Entry{ + {}, + }, }, nil) - dialURLReferral := mockFactory.EXPECT(). - DialURL(gomock.Eq("ldap://192.168.2.1"), gomock.Any()). - Return(mockClientReferral, nil) - - connBindReferral := mockClientReferral.EXPECT(). - Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). - Return(nil) - - connCloseReferral := mockClientReferral.EXPECT().Close() - - searchProfileReferral := mockClientReferral.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -1099,12 +2126,11 @@ func TestShouldReturnUsernameFromLDAPWithReferrals(t *testing.T) { }, }, nil) - gomock.InOrder(dialURL, connBind, searchProfile, dialURLReferral, connBindReferral, searchProfileReferral, connCloseReferral, searchGroups, connClose) + gomock.InOrder(dialURL, connBind, searchProfile, searchGroups, connClose) details, err := provider.GetDetails("john") require.NoError(t, err) - assert.ElementsMatch(t, details.Groups, []string{"group1", "group2"}) assert.ElementsMatch(t, details.Emails, []string{"test@example.com"}) assert.Equal(t, details.DisplayName, "John Doe") assert.Equal(t, details.Username, "John") @@ -1121,16 +2147,20 @@ func TestShouldReturnUsernameFromLDAPWithReferralsInErrorAndResult(t *testing.T) provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -1148,17 +2178,17 @@ func TestShouldReturnUsernameFromLDAPWithReferralsInErrorAndResult(t *testing.T) searchGroups := mockClient.EXPECT(). Search(gomock.Any()). - Return(createSearchResultWithAttributeValues("group1", "group2"), nil) + Return(createGroupSearchResultModeFilter(provider.config.Attributes.GroupName, "group1", "group2"), nil) searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{}, - Referrals: []string{"ldap://192.168.2.1"}, + Referrals: []string{"ldap://192.168.0.1"}, }, &ldap.Error{ResultCode: ldap.LDAPResultReferral, Err: errors.New("referral"), Packet: &testBERPacketReferral}) dialURLReferral := mockFactory.EXPECT(). - DialURL(gomock.Eq("ldap://192.168.2.1"), gomock.Any()). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). Return(mockClientReferral, nil) connBindReferral := mockClientReferral.EXPECT(). @@ -1236,7 +2266,7 @@ func TestShouldReturnUsernameFromLDAPWithReferralsInErrorAndResult(t *testing.T) assert.Equal(t, details.Username, "John") } -func TestShouldReturnUsernameFromLDAPWithReferralsErr(t *testing.T) { +func TestShouldReturnUsernameFromLDAPWithReferralsInErrorAndNoResult(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -1246,16 +2276,20 @@ func TestShouldReturnUsernameFromLDAPWithReferralsErr(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -1273,7 +2307,267 @@ func TestShouldReturnUsernameFromLDAPWithReferralsErr(t *testing.T) { searchGroups := mockClient.EXPECT(). Search(gomock.Any()). - Return(createSearchResultWithAttributeValues("group1", "group2"), nil) + Return(createGroupSearchResultModeFilter(provider.config.Attributes.GroupName, "group1", "group2"), nil) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(nil, &ldap.Error{ResultCode: ldap.LDAPResultReferral, Err: errors.New("referral"), Packet: &testBERPacketReferral}) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + searchProfileReferral := mockClientReferral.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "uid", + Values: []string{"John"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, connBind, searchProfile, dialURLReferral, connBindReferral, searchProfileReferral, connCloseReferral, searchGroups, connClose) + + details, err := provider.GetDetails("john") + require.NoError(t, err) + + assert.ElementsMatch(t, details.Groups, []string{"group1", "group2"}) + assert.ElementsMatch(t, details.Emails, []string{"test@example.com"}) + assert.Equal(t, details.DisplayName, "John Doe") + assert.Equal(t, details.Username, "John") +} + +func TestShouldReturnDialErrDuringReferralSearchUsernameFromLDAPWithReferralsInErrorAndNoResult(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(nil, &ldap.Error{ResultCode: ldap.LDAPResultReferral, Err: errors.New("referral"), Packet: &testBERPacketReferral}) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(nil, fmt.Errorf("failed to connect")) + + gomock.InOrder(dialURL, connBind, searchProfile, dialURLReferral, connClose) + + details, err := provider.GetDetails("john") + + assert.Nil(t, details) + assert.EqualError(t, err, "cannot find user DN of user 'john'. Cause: error occurred connecting to referred LDAP server 'ldap://192.168.0.1': dial failed with error: failed to connect") +} + +func TestShouldReturnSearchErrDuringReferralSearchUsernameFromLDAPWithReferralsInErrorAndNoResult(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(nil, &ldap.Error{ResultCode: ldap.LDAPResultReferral, Err: errors.New("referral"), Packet: &testBERPacketReferral}) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + searchProfileReferral := mockClientReferral.EXPECT(). + Search(gomock.Any()). + Return(nil, fmt.Errorf("not found")) + + gomock.InOrder(dialURL, connBind, searchProfile, dialURLReferral, connBindReferral, searchProfileReferral, connCloseReferral, connClose) + + details, err := provider.GetDetails("john") + + assert.Nil(t, details) + assert.EqualError(t, err, "cannot find user DN of user 'john'. Cause: error occurred performing search on referred LDAP server 'ldap://192.168.0.1': not found") +} + +func TestShouldNotReturnUsernameFromLDAPWithReferralsInErrorAndReferralsNotPermitted(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: false, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(nil, &ldap.Error{ResultCode: ldap.LDAPResultReferral, Err: errors.New("referral"), Packet: &testBERPacketReferral}) + + gomock.InOrder(dialURL, connBind, searchProfile, connClose) + + details, err := provider.GetDetails("john") + assert.EqualError(t, err, "cannot find user DN of user 'john'. Cause: LDAP Result Code 10 \"Referral\": referral") + assert.Nil(t, details) +} + +func TestShouldReturnUsernameFromLDAPWithReferralsErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchGroups := mockClient.EXPECT(). + Search(gomock.Any()). + Return(createGroupSearchResultModeFilter(provider.config.Attributes.GroupName, "group1", "group2"), nil) searchProfile := mockClient.EXPECT(). Search(gomock.Any()). @@ -1333,16 +2627,19 @@ func TestShouldNotUpdateUserPasswordConnect(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: false, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: false, }, false, nil, @@ -1400,16 +2697,19 @@ func TestShouldNotUpdateUserPasswordGetDetails(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: false, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: false, }, false, nil, @@ -1477,15 +2777,18 @@ func TestShouldUpdateUserPassword(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -1584,16 +2887,19 @@ func TestShouldUpdateUserPasswordMSAD(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "activedirectory", - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Implementation: "activedirectory", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -1694,17 +3000,20 @@ func TestShouldUpdateUserPasswordMSADWithReferrals(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "activedirectory", - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Implementation: "activedirectory", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -1822,17 +3131,20 @@ func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralConnectErr(t *test provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "activedirectory", - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Implementation: "activedirectory", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -1941,17 +3253,20 @@ func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralModifyErr(t *testi provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "activedirectory", - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Implementation: "activedirectory", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -2073,17 +3388,20 @@ func TestShouldUpdateUserPasswordMSADWithoutReferrals(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "activedirectory", - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: false, + Implementation: "activedirectory", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: false, }, false, nil, @@ -2187,15 +3505,18 @@ func TestShouldUpdateUserPasswordPasswdModifyExtension(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -2294,16 +3615,19 @@ func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferrals(t *testing.T provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -2421,16 +3745,19 @@ func TestShouldUpdateUserPasswordPasswdModifyExtensionWithoutReferrals(t *testin provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: false, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: false, }, false, nil, @@ -2534,16 +3861,19 @@ func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferralsReferralConne provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -2652,16 +3982,19 @@ func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferralsReferralPassw provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - PermitReferrals: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -2783,16 +4116,20 @@ func TestShouldUpdateUserPasswordActiveDirectoryWithServerPolicyHints(t *testing provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "activedirectory", - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "sAMAccountName", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "cn={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Implementation: "activedirectory", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + Username: "sAMAccountName", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "cn={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -2894,16 +4231,20 @@ func TestShouldUpdateUserPasswordActiveDirectoryWithServerPolicyHintsDeprecated( provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "activedirectory", - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "sAMAccountName", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "cn={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Implementation: "activedirectory", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + Username: "sAMAccountName", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "cn={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3005,16 +4346,20 @@ func TestShouldUpdateUserPasswordActiveDirectory(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "activedirectory", - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "sAMAccountName", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "cn={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Implementation: "activedirectory", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + DistinguishedName: "distinguishedName", + Username: "sAMAccountName", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "cn={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3116,16 +4461,19 @@ func TestShouldUpdateUserPasswordBasic(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Implementation: "custom", - Address: testLDAPAddress, - User: "uid=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Implementation: "custom", + Address: testLDAPAddress, + User: "uid=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3224,15 +4572,18 @@ func TestShouldReturnErrorWhenMultipleUsernameAttributes(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3290,15 +4641,18 @@ func TestShouldReturnErrorWhenZeroUsernameAttributes(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3356,15 +4710,18 @@ func TestShouldReturnErrorWhenUsernameAttributeNotReturned(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3418,15 +4775,18 @@ func TestShouldReturnErrorWhenMultipleUsersFound(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "(|(uid={input})(uid=*))", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "(|(uid={input})(uid=*))", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3501,15 +4861,18 @@ func TestShouldReturnErrorWhenNoDN(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "(|(uid={input})(uid=*))", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "(|(uid={input})(uid=*))", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3567,15 +4930,18 @@ func TestShouldCheckValidUserPassword(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3635,15 +5001,18 @@ func TestShouldNotCheckValidUserPasswordWithConnectError(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3665,6 +5034,51 @@ func TestShouldNotCheckValidUserPasswordWithConnectError(t *testing.T) { assert.EqualError(t, err, "bind failed with error: LDAP Result Code 49 \"Invalid Credentials\": invalid username or password") } +func TestShouldNotCheckValidUserPasswordWithGetProfileError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + provider := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackend{ + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + false, + nil, + mockFactory) + + gomock.InOrder( + mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil), + mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil), + mockClient.EXPECT(). + Search(gomock.Any()). + Return(nil, &ldap.Error{ResultCode: ldap.LDAPResultBusy, Err: errors.New("directory server busy")}), + mockClient.EXPECT().Close(), + ) + + valid, err := provider.CheckUserPassword("john", "password") + + assert.False(t, valid) + assert.EqualError(t, err, "cannot find user DN of user 'john'. Cause: LDAP Result Code 51 \"Busy\": directory server busy") +} + func TestShouldCheckInvalidUserPassword(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -3674,15 +5088,18 @@ func TestShouldCheckInvalidUserPassword(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", }, false, nil, @@ -3742,16 +5159,20 @@ func TestShouldCallStartTLSWhenEnabled(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - StartTLS: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + StartTLS: true, }, false, nil, @@ -3817,18 +5238,21 @@ func TestShouldParseDynamicConfiguration(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:microsoft-nt})(accountExpires>={date-time:generalized})))", - GroupsFilter: "(&(|(member={dn})(member={input})(member={username}))(objectClass=group))", - AdditionalUsersDN: "ou=users", - AdditionalGroupsDN: "ou=groups", - BaseDN: "dc=example,dc=com", - StartTLS: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpiresM>={date-time:microsoft-nt})(accountExpiresU>={date-time:unix})(accountExpiresG>={date-time:generalized})))", + GroupsFilter: "(&(|(member={dn})(member={input})(member={username}))(objectClass=group))", + AdditionalUsersDN: "ou=users", + AdditionalGroupsDN: "ou=groups", + BaseDN: "dc=example,dc=com", + StartTLS: true, }, false, nil, @@ -3848,12 +5272,12 @@ func TestShouldParseDynamicConfiguration(t *testing.T) { assert.True(t, provider.usersFilterReplacementDateTimeGeneralized) assert.True(t, provider.usersFilterReplacementDateTimeMicrosoftNTTimeEpoch) - assert.Equal(t, "(&(|(uid={input})(mail={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:microsoft-nt})(accountExpires>={date-time:generalized})))", provider.config.UsersFilter) + assert.Equal(t, "(&(|(uid={input})(mail={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpiresM>={date-time:microsoft-nt})(accountExpiresU>={date-time:unix})(accountExpiresG>={date-time:generalized})))", provider.config.UsersFilter) assert.Equal(t, "(&(|(member={dn})(member={input})(member={username}))(objectClass=group))", provider.config.GroupsFilter) assert.Equal(t, "ou=users,dc=example,dc=com", provider.usersBaseDN) assert.Equal(t, "ou=groups,dc=example,dc=com", provider.groupsBaseDN) - assert.Equal(t, "(&(|(uid=test@example.com)(mail=test@example.com))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>=133147241190000000)(accountExpires>=20221205142839.0Z)))", provider.resolveUsersFilter("test@example.com")) + assert.Equal(t, "(&(|(uid=test@example.com)(mail=test@example.com))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpiresM>=133147241190000000)(accountExpiresU>=1670250519)(accountExpiresG>=20221205142839.0Z)))", provider.resolveUsersFilter("test@example.com")) assert.Equal(t, "(&(|(member=cn=admin,dc=example,dc=com)(member=test@example.com)(member=test))(objectClass=group))", provider.resolveGroupsFilter("test@example.com", &ldapUserProfile{Username: "test", DN: "cn=admin,dc=example,dc=com"})) } @@ -3866,16 +5290,20 @@ func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - StartTLS: true, + Address: testLDAPAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + GroupName: "cn", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + StartTLS: true, TLS: &schema.TLSConfig{ SkipVerify: true, }, @@ -3924,6 +5352,10 @@ func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T Name: "uid", Values: []string{"john"}, }, + { + Name: "memberOf", + Values: []string{"CN=example,DC=corp,DC=com"}, + }, }, }, }, @@ -3949,16 +5381,19 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) { provider := NewLDAPUserProviderWithFactory( schema.LDAPAuthenticationBackend{ - Address: testLDAPSAddress, - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - StartTLS: true, + Address: testLDAPSAddress, + User: "cn=admin,dc=example,dc=com", + Password: "password", + Attributes: schema.LDAPAuthenticationAttributes{ + Username: "uid", + Mail: "mail", + DisplayName: "displayName", + MemberOf: "memberOf", + }, + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + StartTLS: true, TLS: &schema.TLSConfig{ SkipVerify: true, }, diff --git a/internal/authentication/ldap_util.go b/internal/authentication/ldap_util.go index 3c6025ec6..c19be6203 100644 --- a/internal/authentication/ldap_util.go +++ b/internal/authentication/ldap_util.go @@ -5,7 +5,7 @@ import ( "strings" ber "github.com/go-asn1-ber/asn1-ber" - "github.com/go-ldap/ldap/v3" + ldap "github.com/go-ldap/ldap/v3" ) func ldapEntriesContainsEntry(needle *ldap.Entry, haystack []*ldap.Entry) bool { @@ -67,12 +67,12 @@ func ldapEscape(inputUsername string) string { } func ldapGetReferral(err error) (referral string, ok bool) { - if !ldap.IsErrorWithCode(err, ldap.LDAPResultReferral) { - return "", false - } - switch e := err.(type) { case *ldap.Error: + if e.ResultCode != ldap.LDAPResultReferral { + return "", false + } + if e.Packet == nil { return "", false } diff --git a/internal/authentication/ldap_util_test.go b/internal/authentication/ldap_util_test.go index cc3a04237..cb4dc2746 100644 --- a/internal/authentication/ldap_util_test.go +++ b/internal/authentication/ldap_util_test.go @@ -193,6 +193,12 @@ func TestLDAPGetReferral(t *testing.T) { expectedReferral: "", expectedOK: false, }, + { + description: "ShouldNotGetInvalidErrType", + have: errors.New("not an err"), + expectedReferral: "", + expectedOK: false, + }, } for _, tc := range testCases { diff --git a/internal/authentication/types.go b/internal/authentication/types.go index e210bb10f..64cc61b30 100644 --- a/internal/authentication/types.go +++ b/internal/authentication/types.go @@ -82,6 +82,7 @@ type ldapUserProfile struct { Emails []string DisplayName string Username string + MemberOf []string } // LDAPSupportedFeatures represents features which a server may support which are implemented in code. diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 61e010094..f4cedef98 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -315,7 +315,7 @@ authentication_backend: ## because it allows Authelia to offload the stateful operations ## onto the LDAP service. # ldap: - ## The address of the LDAP server to connect to in the address common syntax. + ## The address of the directory server to connect to in the address common syntax. ## Format: [://][:]. ## Square brackets indicate optional portions of the format. Scheme must be 'ldap', 'ldaps', or 'ldapi`. ## The default scheme is 'ldapi' if the address is an absolute path otherwise it's 'ldaps'. @@ -401,16 +401,6 @@ authentication_backend: ## See also: additional_users_dn, additional_groups_dn. # base_dn: 'dc=example,dc=com' - ## The attribute holding the username of the user. This attribute is used to populate the username in the session - ## information. For your information, Microsoft Active Directory usually uses 'sAMAccountName' and OpenLDAP usually - ## uses 'uid'. Beware that this attribute holds the unique identifiers for the users binding the user and the - ## configuration stored in database. Therefore only single value attributes are allowed and the value must never be - ## changed once attributed to a user otherwise it would break the configuration for that user. Technically, - ## non-unique attributes like 'mail' can also be used but we don't recommend using them, we instead advise to use - ## a filter to perform alternative lookups and the attributes mentioned above (sAMAccountName and uid) to - ## follow https://datatracker.ietf.org/doc/html/rfc2307. - # username_attribute: 'uid' - ## The additional_users_dn is prefixed to base_dn and delimited by a comma when searching for users. ## i.e. with this set to OU=Users and base_dn set to DC=a,DC=com; OU=Users,DC=a,DC=com is searched for users. # additional_users_dn: 'ou=users' @@ -441,15 +431,9 @@ authentication_backend: ## (&(uniqueMember={dn})(objectClass=groupOfUniqueNames)) # groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - ## The attribute holding the name of the group. - # group_name_attribute: 'cn' - - ## The attribute holding the mail address of the user. If multiple email addresses are defined for a user, only the - ## first one returned by the LDAP server is used. - # mail_attribute: 'mail' - - ## The attribute holding the display name of the user. This will be used to greet an authenticated user. - # display_name_attribute: 'displayName' + ## The group search mode to use. Options are 'filter' or 'memberof'. It's essential to read the docs if you wish to + ## use 'memberof'. Also 'filter' is the best choice for most use cases. + # group_search_mode: 'filter' ## Follow referrals returned by the server. ## This is especially useful for environments where read-only servers exist. Only implemented for write operations. @@ -460,6 +444,37 @@ authentication_backend: ## Password can also be set using a secret: https://www.authelia.com/c/secrets # password: 'password' + ## The attributes for users and objects from the directory server. + # attributes: + + ## The distinguished name attribute if your directory server supports it. Users should read the docs before + ## configuring. Only used for the 'memberof' group search mode. + # distinguished_name: '' + + ## The attribute holding the username of the user. This attribute is used to populate the username in the session + ## information. For your information, Microsoft Active Directory usually uses 'sAMAccountName' and OpenLDAP + ## usually uses 'uid'. Beware that this attribute holds the unique identifiers for the users binding the user and + ## the configuration stored in database; therefore only single value attributes are allowed and the value must + ## never be changed once attributed to a user otherwise it would break the configuration for that user. + ## Technically non-unique attributes like 'mail' can also be used but we don't recommend using them, we instead + ## advise to use a filter to perform alternative lookups and the attributes mentioned above + ## (sAMAccountName and uid) to follow https://datatracker.ietf.org/doc/html/rfc2307. + # username: 'uid' + + ## The attribute holding the display name of the user. This will be used to greet an authenticated user. + # display_name: 'displayName' + + ## The attribute holding the mail address of the user. If multiple email addresses are defined for a user, only + ## the first one returned by the directory server is used. + # mail: 'mail' + + ## The attribute which provides distinguished names of groups an object is a member of. + ## Only used for the 'memberof' group search mode. + # member_of: 'memberOf' + + ## The attribute holding the name of the group. + # group_name: 'cn' + ## ## File (Authentication Provider) ## diff --git a/internal/configuration/deprecation.go b/internal/configuration/deprecation.go index eeaa84a2e..b8e9d472e 100644 --- a/internal/configuration/deprecation.go +++ b/internal/configuration/deprecation.go @@ -294,4 +294,36 @@ var deprecations = map[string]Deprecation{ MapFunc: nil, ErrFunc: nil, }, + "authentication_backend.ldap.username_attribute": { + Version: model.SemanticVersion{Major: 4, Minor: 38}, + Key: "authentication_backend.ldap.username_attribute", + NewKey: "authentication_backend.ldap.attributes.username", + AutoMap: true, + MapFunc: nil, + ErrFunc: nil, + }, + "authentication_backend.ldap.mail_attribute": { + Version: model.SemanticVersion{Major: 4, Minor: 38}, + Key: "authentication_backend.ldap.mail_attribute", + NewKey: "authentication_backend.ldap.attributes.mail", + AutoMap: true, + MapFunc: nil, + ErrFunc: nil, + }, + "authentication_backend.ldap.display_name_attribute": { + Version: model.SemanticVersion{Major: 4, Minor: 38}, + Key: "authentication_backend.ldap.display_name_attribute", + NewKey: "authentication_backend.ldap.attributes.display_name", + AutoMap: true, + MapFunc: nil, + ErrFunc: nil, + }, + "authentication_backend.ldap.group_name_attribute": { + Version: model.SemanticVersion{Major: 4, Minor: 38}, + Key: "authentication_backend.ldap.group_name_attribute", + NewKey: "authentication_backend.ldap.attributes.group_name", + AutoMap: true, + MapFunc: nil, + ErrFunc: nil, + }, } diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 1072a6956..0e1259b05 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -245,6 +245,28 @@ func TestShouldLoadURLList(t *testing.T) { assert.Equal(t, "https://example.com", config.IdentityProviders.OIDC.CORS.AllowedOrigins[1].String()) } +func TestShouldDisableOIDCEntropy(t *testing.T) { + val := schema.NewStructValidator() + keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_oidc_disable_entropy.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) + + assert.NoError(t, err) + + validator.ValidateKeys(keys, DefaultEnvPrefix, val) + + assert.Len(t, val.Errors(), 0) + assert.Len(t, val.Warnings(), 0) + + assert.Equal(t, -1, config.IdentityProviders.OIDC.MinimumParameterEntropy) + + validator.ValidateIdentityProviders(&config.IdentityProviders, val) + + assert.Len(t, val.Errors(), 1) + require.Len(t, val.Warnings(), 2) + + assert.EqualError(t, val.Warnings()[0], "identity_providers: oidc: option 'minimum_parameter_entropy' is disabled which is considered unsafe and insecure") + assert.Equal(t, -1, config.IdentityProviders.OIDC.MinimumParameterEntropy) +} + func TestShouldConfigureConsent(t *testing.T) { val := schema.NewStructValidator() keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_oidc.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go index 876982498..19b73b2e9 100644 --- a/internal/configuration/schema/authentication.go +++ b/internal/configuration/schema/authentication.go @@ -108,11 +108,9 @@ type LDAPAuthenticationBackend struct { AdditionalGroupsDN string `koanf:"additional_groups_dn"` GroupsFilter string `koanf:"groups_filter"` + GroupSearchMode string `koanf:"group_search_mode"` - GroupNameAttribute string `koanf:"group_name_attribute"` - UsernameAttribute string `koanf:"username_attribute"` - MailAttribute string `koanf:"mail_attribute"` - DisplayNameAttribute string `koanf:"display_name_attribute"` + Attributes LDAPAuthenticationAttributes `koanf:"attributes"` PermitReferrals bool `koanf:"permit_referrals"` PermitUnauthenticatedBind bool `koanf:"permit_unauthenticated_bind"` @@ -122,6 +120,16 @@ type LDAPAuthenticationBackend struct { Password string `koanf:"password"` } +// LDAPAuthenticationAttributes represents the configuration related to LDAP server attributes. +type LDAPAuthenticationAttributes struct { + DistinguishedName string `koanf:"distinguished_name"` + Username string `koanf:"username"` + DisplayName string `koanf:"display_name"` + Mail string `koanf:"mail"` + MemberOf string `koanf:"member_of"` + GroupName string `koanf:"group_name"` +} + // DefaultPasswordConfig represents the default configuration related to Argon2id hashing. var DefaultPasswordConfig = Password{ Algorithm: argon2, @@ -175,11 +183,14 @@ var DefaultCIPasswordConfig = Password{ // DefaultLDAPAuthenticationBackendConfigurationImplementationCustom represents the default LDAP config. var DefaultLDAPAuthenticationBackendConfigurationImplementationCustom = LDAPAuthenticationBackend{ - UsernameAttribute: ldapAttrUserID, - MailAttribute: ldapAttrMail, - DisplayNameAttribute: ldapAttrDisplayName, - GroupNameAttribute: ldapAttrCommonName, - Timeout: time.Second * 5, + GroupSearchMode: ldapGroupSearchModeFilter, + Attributes: LDAPAuthenticationAttributes{ + Username: ldapAttrUserID, + DisplayName: ldapAttrDisplayName, + Mail: ldapAttrMail, + GroupName: ldapAttrCommonName, + }, + Timeout: time.Second * 5, TLS: &TLSConfig{ MinimumVersion: TLSVersion{tls.VersionTLS12}, }, @@ -187,13 +198,18 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationCustom = LDAPAuth // DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory represents the default LDAP config for the LDAPImplementationActiveDirectory Implementation. var DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory = LDAPAuthenticationBackend{ - UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:microsoft-nt})))", - UsernameAttribute: "sAMAccountName", - MailAttribute: ldapAttrMail, - DisplayNameAttribute: ldapAttrDisplayName, - GroupsFilter: "(&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912)))", - GroupNameAttribute: ldapAttrCommonName, - Timeout: time.Second * 5, + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:microsoft-nt})))", + GroupsFilter: "(&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912)))", + GroupSearchMode: ldapGroupSearchModeFilter, + Attributes: LDAPAuthenticationAttributes{ + DistinguishedName: ldapAttrDistinguishedName, + Username: ldapAttrSAMAccountName, + DisplayName: ldapAttrDisplayName, + Mail: ldapAttrMail, + MemberOf: ldapAttrMemberOf, + GroupName: ldapAttrCommonName, + }, + Timeout: time.Second * 5, TLS: &TLSConfig{ MinimumVersion: TLSVersion{tls.VersionTLS12}, }, @@ -201,13 +217,17 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory = // DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis represents the default LDAP config for the LDAPImplementationRFC2307bis Implementation. var DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis = LDAPAuthenticationBackend{ - UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(|(objectClass=inetOrgPerson)(objectClass=organizationalPerson)))", - UsernameAttribute: ldapAttrUserID, - MailAttribute: ldapAttrMail, - DisplayNameAttribute: ldapAttrDisplayName, - GroupsFilter: "(&(|(member={dn})(uniqueMember={dn}))(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)(objectClass=groupOfMembers)))", - GroupNameAttribute: ldapAttrCommonName, - Timeout: time.Second * 5, + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(|(objectClass=inetOrgPerson)(objectClass=organizationalPerson)))", + GroupsFilter: "(&(|(member={dn})(uniqueMember={dn}))(|(objectClass=groupOfNames)(objectClass=groupOfUniqueNames)(objectClass=groupOfMembers))(!(pwdReset=TRUE)))", + GroupSearchMode: ldapGroupSearchModeFilter, + Attributes: LDAPAuthenticationAttributes{ + Username: ldapAttrUserID, + DisplayName: ldapAttrDisplayName, + Mail: ldapAttrMail, + MemberOf: ldapAttrMemberOf, + GroupName: ldapAttrCommonName, + }, + Timeout: time.Second * 5, TLS: &TLSConfig{ MinimumVersion: TLSVersion{tls.VersionTLS12}, }, @@ -215,13 +235,17 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis = LDAP // DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA represents the default LDAP config for the LDAPImplementationFreeIPA Implementation. var DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA = LDAPAuthenticationBackend{ - UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))(krbPasswordExpiration>={date-time:generalized})(|(!(krbPrincipalExpiration=*))(krbPrincipalExpiration>={date-time:generalized})))", - UsernameAttribute: ldapAttrUserID, - MailAttribute: ldapAttrMail, - DisplayNameAttribute: ldapAttrDisplayName, - GroupsFilter: "(&(member={dn})(objectClass=groupOfNames))", - GroupNameAttribute: ldapAttrCommonName, - Timeout: time.Second * 5, + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))(krbPasswordExpiration>={date-time:generalized})(|(!(krbPrincipalExpiration=*))(krbPrincipalExpiration>={date-time:generalized})))", + GroupsFilter: "(&(member={dn})(objectClass=groupOfNames))", + GroupSearchMode: ldapGroupSearchModeFilter, + Attributes: LDAPAuthenticationAttributes{ + Username: ldapAttrUserID, + DisplayName: ldapAttrDisplayName, + Mail: ldapAttrMail, + MemberOf: ldapAttrMemberOf, + GroupName: ldapAttrCommonName, + }, + Timeout: time.Second * 5, TLS: &TLSConfig{ MinimumVersion: TLSVersion{tls.VersionTLS12}, }, @@ -229,15 +253,19 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA = LDAPAut // DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP represents the default LDAP config for the LDAPImplementationLLDAP Implementation. var DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP = LDAPAuthenticationBackend{ - AdditionalUsersDN: "OU=people", - AdditionalGroupsDN: "OU=groups", - UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))", - UsernameAttribute: ldapAttrUserID, - MailAttribute: ldapAttrMail, - DisplayNameAttribute: ldapAttrCommonName, - GroupsFilter: "(&(member={dn})(objectClass=groupOfUniqueNames))", - GroupNameAttribute: ldapAttrCommonName, - Timeout: time.Second * 5, + AdditionalUsersDN: "OU=people", + AdditionalGroupsDN: "OU=groups", + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))", + GroupsFilter: "(&(member={dn})(objectClass=groupOfUniqueNames))", + GroupSearchMode: ldapGroupSearchModeFilter, + Attributes: LDAPAuthenticationAttributes{ + Username: ldapAttrUserID, + DisplayName: ldapAttrCommonName, + Mail: ldapAttrMail, + MemberOf: ldapAttrMemberOf, + GroupName: ldapAttrCommonName, + }, + Timeout: time.Second * 5, TLS: &TLSConfig{ MinimumVersion: TLSVersion{tls.VersionTLS12}, }, @@ -245,13 +273,17 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP = LDAPAuthe // DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth represents the default LDAP config for the LDAPImplementationGLAuth Implementation. var DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth = LDAPAuthenticationBackend{ - UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=posixAccount)(!(accountStatus=inactive)))", - UsernameAttribute: ldapAttrCommonName, - MailAttribute: ldapAttrMail, - DisplayNameAttribute: ldapAttrDescription, - GroupsFilter: "(&(uniqueMember={dn})(objectClass=posixGroup))", - GroupNameAttribute: ldapAttrCommonName, - Timeout: time.Second * 5, + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=posixAccount)(!(accountStatus=inactive)))", + GroupsFilter: "(&(uniqueMember={dn})(objectClass=posixGroup))", + GroupSearchMode: ldapGroupSearchModeFilter, + Attributes: LDAPAuthenticationAttributes{ + Username: ldapAttrCommonName, + DisplayName: ldapAttrDescription, + Mail: ldapAttrMail, + MemberOf: ldapAttrMemberOf, + GroupName: ldapAttrCommonName, + }, + Timeout: time.Second * 5, TLS: &TLSConfig{ MinimumVersion: TLSVersion{tls.VersionTLS12}, }, diff --git a/internal/configuration/schema/const.go b/internal/configuration/schema/const.go index 9c5621c2d..f7575939f 100644 --- a/internal/configuration/schema/const.go +++ b/internal/configuration/schema/const.go @@ -78,6 +78,14 @@ const ( LDAPImplementationGLAuth = "glauth" ) +const ( + // LDAPGroupSearchModeFilter is the string for the filter group search mode. + LDAPGroupSearchModeFilter = "filter" + + // LDAPGroupSearchModeMemberOf is the string for the memberOf group search mode. + LDAPGroupSearchModeMemberOf = "memberof" +) + // TOTP Algorithm. const ( TOTPAlgorithmSHA1 = "SHA1" @@ -121,11 +129,18 @@ const ( ) const ( - ldapAttrMail = "mail" - ldapAttrUserID = "uid" - ldapAttrDisplayName = "displayName" - ldapAttrDescription = "description" - ldapAttrCommonName = "cn" + ldapGroupSearchModeFilter = "filter" +) + +const ( + ldapAttrDistinguishedName = "distinguishedName" + ldapAttrMail = "mail" + ldapAttrUserID = "uid" + ldapAttrSAMAccountName = "sAMAccountName" + ldapAttrDisplayName = "displayName" + ldapAttrDescription = "description" + ldapAttrCommonName = "cn" + ldapAttrMemberOf = "memberOf" ) // Address Schemes. diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index c4c0054e7..df820a3ce 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -120,10 +120,13 @@ var Keys = []string{ "authentication_backend.ldap.users_filter", "authentication_backend.ldap.additional_groups_dn", "authentication_backend.ldap.groups_filter", - "authentication_backend.ldap.group_name_attribute", - "authentication_backend.ldap.username_attribute", - "authentication_backend.ldap.mail_attribute", - "authentication_backend.ldap.display_name_attribute", + "authentication_backend.ldap.group_search_mode", + "authentication_backend.ldap.attributes.distinguished_name", + "authentication_backend.ldap.attributes.username", + "authentication_backend.ldap.attributes.display_name", + "authentication_backend.ldap.attributes.mail", + "authentication_backend.ldap.attributes.member_of", + "authentication_backend.ldap.attributes.group_name", "authentication_backend.ldap.permit_referrals", "authentication_backend.ldap.permit_unauthenticated_bind", "authentication_backend.ldap.permit_feature_detection_failure", diff --git a/internal/configuration/test_resources/config.durations.yml b/internal/configuration/test_resources/config.durations.yml index f20f5d3be..e6c3fe89e 100644 --- a/internal/configuration/test_resources/config.durations.yml +++ b/internal/configuration/test_resources/config.durations.yml @@ -66,14 +66,15 @@ authentication_backend: 1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw== -----END RSA PRIVATE KEY----- base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' access_control: default_policy: 'deny' diff --git a/internal/configuration/test_resources/config.filtered.yml b/internal/configuration/test_resources/config.filtered.yml index aa7f5ca27..6048c6736 100644 --- a/internal/configuration/test_resources/config.filtered.yml +++ b/internal/configuration/test_resources/config.filtered.yml @@ -47,14 +47,15 @@ authentication_backend: 1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw== -----END RSA PRIVATE KEY----- base_dn: dc=example,dc=com - username_attribute: uid additional_users_dn: ou=users users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user)) additional_groups_dn: ou=groups groups_filter: (&(member={dn})(objectClass=groupOfNames)) - group_name_attribute: cn - mail_attribute: mail user: cn=admin,dc=example,dc=com + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' access_control: default_policy: deny diff --git a/internal/configuration/test_resources/config.yml b/internal/configuration/test_resources/config.yml index 816e422ce..467b46351 100644 --- a/internal/configuration/test_resources/config.yml +++ b/internal/configuration/test_resources/config.yml @@ -66,14 +66,15 @@ authentication_backend: 1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw== -----END RSA PRIVATE KEY----- base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' access_control: default_policy: 'deny' diff --git a/internal/configuration/test_resources/config_alt.yml b/internal/configuration/test_resources/config_alt.yml index ec9177f72..6595e4bbf 100644 --- a/internal/configuration/test_resources/config_alt.yml +++ b/internal/configuration/test_resources/config_alt.yml @@ -18,14 +18,16 @@ authentication_backend: ldap: address: 'ldap://127.0.0.1' base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' + user: 'cn=admin,dc=example,dc=com' + attributes: + mail: 'mail' + group_name: 'cn' + username: 'uid' access_control: default_policy: 'deny' diff --git a/internal/configuration/test_resources/config_bad_keys.yml b/internal/configuration/test_resources/config_bad_keys.yml index fc898ac75..8b877b5e4 100644 --- a/internal/configuration/test_resources/config_bad_keys.yml +++ b/internal/configuration/test_resources/config_bad_keys.yml @@ -19,14 +19,15 @@ authentication_backend: ldap: address: 'ldap://127.0.0.1' base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' access_control: default_policy: 'deny' diff --git a/internal/configuration/test_resources/config_domain_bad_regex.yml b/internal/configuration/test_resources/config_domain_bad_regex.yml index 7c8250dae..35b119848 100644 --- a/internal/configuration/test_resources/config_domain_bad_regex.yml +++ b/internal/configuration/test_resources/config_domain_bad_regex.yml @@ -18,14 +18,15 @@ authentication_backend: ldap: address: 'ldap://127.0.0.1' base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' access_control: default_policy: 'deny' diff --git a/internal/configuration/test_resources/config_domain_regex.yml b/internal/configuration/test_resources/config_domain_regex.yml index 78a486d21..b68e51d3e 100644 --- a/internal/configuration/test_resources/config_domain_regex.yml +++ b/internal/configuration/test_resources/config_domain_regex.yml @@ -18,13 +18,15 @@ authentication_backend: ldap: address: 'ldap://127.0.0.1' base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' + attributes: + group_name: 'cn' + mail: 'mail' + username: 'uid' + user: 'cn=admin,dc=example,dc=com' access_control: diff --git a/internal/configuration/test_resources/config_oidc.yml b/internal/configuration/test_resources/config_oidc.yml index 4ddb34421..061bbaa19 100644 --- a/internal/configuration/test_resources/config_oidc.yml +++ b/internal/configuration/test_resources/config_oidc.yml @@ -18,14 +18,15 @@ authentication_backend: ldap: address: 'ldap://127.0.0.1' base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' access_control: default_policy: 'deny' diff --git a/internal/configuration/test_resources/config_oidc_disable_entropy.yml b/internal/configuration/test_resources/config_oidc_disable_entropy.yml new file mode 100644 index 000000000..4d2cb52e3 --- /dev/null +++ b/internal/configuration/test_resources/config_oidc_disable_entropy.yml @@ -0,0 +1,137 @@ +--- +default_redirection_url: 'https://home.example.com:8080/' + +server: + address: 'tcp://127.0.0.1:9091' + +log: + level: 'debug' + +totp: + issuer: 'authelia.com' + +duo_api: + hostname: 'api-123456789.example.com' + integration_key: 'ABCDEF' + +authentication_backend: + ldap: + address: 'ldap://127.0.0.1' + base_dn: 'dc=example,dc=com' + additional_users_dn: 'ou=users' + users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' + additional_groups_dn: 'ou=groups' + groups_filter: '(&(member={dn})(objectClass=groupOfNames))' + user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' + +access_control: + default_policy: 'deny' + + rules: + # Rules applied to everyone + - domain: 'public.example.com' + policy: 'bypass' + + - domain: 'secure.example.com' + policy: 'one_factor' + # Network based rule, if not provided any network matches. + networks: + - '192.168.1.0/24' + - domain: 'secure.example.com' + policy: 'two_factor' + + - domain: ['singlefactor.example.com', 'onefactor.example.com'] + policy: 'one_factor' + + # Rules applied to 'admins' group + - domain: 'mx2.mail.example.com' + subject: 'group:admins' + policy: 'deny' + - domain: '*.example.com' + subject: 'group:admins' + policy: 'two_factor' + + # Rules applied to 'dev' group + - domain: 'dev.example.com' + resources: + - '^/groups/dev/.*$' + subject: 'group:dev' + policy: 'two_factor' + + # Rules applied to user 'john' + - domain: 'dev.example.com' + resources: + - '^/users/john/.*$' + subject: 'user:john' + policy: 'two_factor' + + # Rules applied to 'dev' group and user 'john' + - domain: 'dev.example.com' + resources: + - '^/deny-all.*$' + subject: ['group:dev', 'user:john'] + policy: 'deny' + + # Rules applied to user 'harry' + - domain: 'dev.example.com' + resources: + - '^/users/harry/.*$' + subject: 'user:harry' + policy: 'two_factor' + + # Rules applied to user 'bob' + - domain: '*.mail.example.com' + subject: 'user:bob' + policy: 'two_factor' + - domain: 'dev.example.com' + resources: + - '^/users/bob/.*$' + subject: 'user:bob' + policy: 'two_factor' + +session: + name: 'authelia_session' + expiration: '1h' # 1 hour + inactivity: '5m' # 5 minutes + domain: 'example.com' + redis: + host: '127.0.0.1' + port: 6379 + high_availability: + sentinel_name: 'test' + +regulation: + max_retries: 3 + find_time: '2m' + ban_time: '5m' + +storage: + mysql: + address: 'tcp://127.0.0.1:3306' + database: 'authelia' + username: 'authelia' + +notifier: + smtp: + address: 'smtp://127.0.0.1:1025' + username: 'test' + sender: 'admin@example.com' + disable_require_tls: true + +identity_providers: + oidc: + cors: + allowed_origins: + - 'https://google.com' + - 'https://example.com' + minimum_parameter_entropy: -1 + clients: + - id: 'abc' + secret: '123' + consent_mode: 'explicit' + userinfo_signing_alg: 'none' +... diff --git a/internal/configuration/test_resources/config_oidc_modern.yml b/internal/configuration/test_resources/config_oidc_modern.yml index 56a455505..67f6f1427 100644 --- a/internal/configuration/test_resources/config_oidc_modern.yml +++ b/internal/configuration/test_resources/config_oidc_modern.yml @@ -1,108 +1,111 @@ --- -default_redirection_url: https://home.example.com:8080/ +default_redirection_url: 'https://home.example.com:8080/' server: host: 127.0.0.1 port: 9091 log: - level: debug + level: 'debug' totp: - issuer: authelia.com + issuer: 'authelia.com' duo_api: - hostname: api-123456789.example.com - integration_key: ABCDEF + hostname: 'api-123456789.example.com' + integration_key: 'ABCDEF' authentication_backend: ldap: - url: ldap://127.0.0.1 - base_dn: dc=example,dc=com - username_attribute: uid - additional_users_dn: ou=users - users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user)) - additional_groups_dn: ou=groups - groups_filter: (&(member={dn})(objectClass=groupOfNames)) - group_name_attribute: cn - mail_attribute: mail - user: cn=admin,dc=example,dc=com + url: 'ldap://127.0.0.1' + base_dn: 'dc=example,dc=com' + additional_users_dn: 'ou=users' + users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' + additional_groups_dn: 'ou=groups' + groups_filter: '(&(member={dn})(objectClass=groupOfNames))' + user: 'cn=admin,dc=example,dc=com' + attributes: + mail: 'mail' + username: 'uid' + group_name: 'cn' access_control: - default_policy: deny + default_policy: 'deny' rules: # Rules applied to everyone - - domain: public.example.com - policy: bypass + - domain: 'public.example.com' + policy: 'bypass' - - domain: secure.example.com - policy: one_factor + - domain: 'secure.example.com' + policy: 'one_factor' # Network based rule, if not provided any network matches. networks: - - 192.168.1.0/24 - - domain: secure.example.com - policy: two_factor + - '192.168.1.0/24' + - domain: 'secure.example.com' + policy: 'two_factor' - - domain: [singlefactor.example.com, onefactor.example.com] - policy: one_factor + - domain: + - 'singlefactor.example.com' + - 'onefactor.example.com' + policy: 'one_factor' # Rules applied to 'admins' group - - domain: "mx2.mail.example.com" - subject: "group:admins" - policy: deny - - domain: "*.example.com" - subject: "group:admins" - policy: two_factor + - domain: 'mx2.mail.example.com' + subject: 'group:admins' + policy: 'deny' + - domain: '*.example.com' + subject: 'group:admins' + policy: 'two_factor' # Rules applied to 'dev' group - - domain: dev.example.com + - domain: 'dev.example.com' resources: - - "^/groups/dev/.*$" - subject: "group:dev" - policy: two_factor + - '^/groups/dev/.*$' + subject: 'group:dev' + policy: 'two_factor' # Rules applied to user 'john' - - domain: dev.example.com + - domain: 'dev.example.com' resources: - - "^/users/john/.*$" - subject: "user:john" - policy: two_factor + - '^/users/john/.*$' + subject: 'user:john' + policy: 'two_factor' # Rules applied to 'dev' group and user 'john' - - domain: dev.example.com + - domain: 'dev.example.com' resources: - - "^/deny-all.*$" - subject: ["group:dev", "user:john"] - policy: deny + - '^/deny-all.*$' + subject: ['group:dev', 'user:john'] + policy: 'deny' # Rules applied to user 'harry' - - domain: dev.example.com + - domain: 'dev.example.com' resources: - - "^/users/harry/.*$" - subject: "user:harry" - policy: two_factor + - '^/users/harry/.*$' + subject: 'user:harry' + policy: 'two_factor' # Rules applied to user 'bob' - - domain: "*.mail.example.com" - subject: "user:bob" - policy: two_factor - - domain: "dev.example.com" + - domain: '*.mail.example.com' + subject: 'user:bob' + policy: 'two_factor' + - domain: 'dev.example.com' resources: - - "^/users/bob/.*$" - subject: "user:bob" - policy: two_factor + - '^/users/bob/.*$' + subject: 'user:bob' + policy: 'two_factor' session: - name: authelia_session + name: 'authelia_session' expiration: 3600000 # 1 hour inactivity: 300000 # 5 minutes - domain: example.com + domain: 'example.com' redis: host: 127.0.0.1 port: 6379 high_availability: - sentinel_name: test + sentinel_name: 'test' regulation: max_retries: 3 @@ -113,12 +116,12 @@ storage: mysql: host: 127.0.0.1 port: 3306 - database: authelia - username: authelia + database: 'authelia' + username: 'authelia' notifier: smtp: - username: test + username: 'test' host: 127.0.0.1 port: 1025 sender: admin@example.com @@ -126,7 +129,7 @@ notifier: identity_providers: oidc: - hmac_secret: 1nb2j3kh1b23kjh1b23jh1b23j1h2b3 + hmac_secret: '1nb2j3kh1b23kjh1b23jh1b23j1h2b3' issuer_private_keys: keys: keya: @@ -202,10 +205,10 @@ identity_providers: -----END CERTIFICATE----- cors: allowed_origins: - - https://google.com - - https://example.com + - 'https://google.com' + - 'https://example.com' clients: - - id: abc + - id: 'abc' secret: '123' - consent_mode: explicit + consent_mode: 'explicit' ... diff --git a/internal/configuration/test_resources/config_smtp_sender_blank.yml b/internal/configuration/test_resources/config_smtp_sender_blank.yml index 0fe8205a5..e3c94fb06 100644 --- a/internal/configuration/test_resources/config_smtp_sender_blank.yml +++ b/internal/configuration/test_resources/config_smtp_sender_blank.yml @@ -18,14 +18,15 @@ authentication_backend: ldap: address: 'ldap://127.0.0.1' base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' access_control: default_policy: 'deny' diff --git a/internal/configuration/test_resources/config_smtp_sender_invalid.yml b/internal/configuration/test_resources/config_smtp_sender_invalid.yml index 8d2b55a35..0c8fcd362 100644 --- a/internal/configuration/test_resources/config_smtp_sender_invalid.yml +++ b/internal/configuration/test_resources/config_smtp_sender_invalid.yml @@ -18,14 +18,15 @@ authentication_backend: ldap: address: 'ldap://127.0.0.1' base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' access_control: default_policy: 'deny' diff --git a/internal/configuration/test_resources/config_with_secret.yml b/internal/configuration/test_resources/config_with_secret.yml index aa2b3acc9..c07f00476 100644 --- a/internal/configuration/test_resources/config_with_secret.yml +++ b/internal/configuration/test_resources/config_with_secret.yml @@ -20,14 +20,16 @@ authentication_backend: ldap: address: 'ldap://127.0.0.1' base_dn: 'dc=example,dc=com' - username_attribute: 'uid' additional_users_dn: 'ou=users' users_filter: '(&({username_attribute}={input})(objectCategory=person)(objectClass=user))' additional_groups_dn: 'ou=groups' groups_filter: '(&(member={dn})(objectClass=groupOfNames))' - group_name_attribute: 'cn' - mail_attribute: 'mail' user: 'cn=admin,dc=example,dc=com' + attributes: + username: 'uid' + group_name: 'cn' + mail: 'mail' + access_control: default_policy: 'deny' diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index cf90832bc..4e7c5305f 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -364,7 +364,7 @@ func validateLDAPAuthenticationBackendImplementation(config *schema.Authenticati case schema.LDAPImplementationGLAuth: implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth default: - validator.Push(fmt.Errorf(errFmtLDAPAuthBackendImplementation, strJoinOr(validLDAPImplementations), config.LDAP.Implementation)) + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendOptionMustBeOneOf, "implementation", strJoinOr(validLDAPImplementations), config.LDAP.Implementation)) } tlsconfig := &schema.TLSConfig{} @@ -394,32 +394,44 @@ func setDefaultImplementationLDAPAuthenticationBackendProfileAttributes(config * config.AdditionalUsersDN = implementation.AdditionalUsersDN } - if ldapImplementationShouldSetStr(config.AdditionalGroupsDN, implementation.AdditionalGroupsDN) { - config.AdditionalGroupsDN = implementation.AdditionalGroupsDN - } - if ldapImplementationShouldSetStr(config.UsersFilter, implementation.UsersFilter) { config.UsersFilter = implementation.UsersFilter } - if ldapImplementationShouldSetStr(config.UsernameAttribute, implementation.UsernameAttribute) { - config.UsernameAttribute = implementation.UsernameAttribute - } - - if ldapImplementationShouldSetStr(config.DisplayNameAttribute, implementation.DisplayNameAttribute) { - config.DisplayNameAttribute = implementation.DisplayNameAttribute - } - - if ldapImplementationShouldSetStr(config.MailAttribute, implementation.MailAttribute) { - config.MailAttribute = implementation.MailAttribute + if ldapImplementationShouldSetStr(config.AdditionalGroupsDN, implementation.AdditionalGroupsDN) { + config.AdditionalGroupsDN = implementation.AdditionalGroupsDN } if ldapImplementationShouldSetStr(config.GroupsFilter, implementation.GroupsFilter) { config.GroupsFilter = implementation.GroupsFilter } - if ldapImplementationShouldSetStr(config.GroupNameAttribute, implementation.GroupNameAttribute) { - config.GroupNameAttribute = implementation.GroupNameAttribute + if ldapImplementationShouldSetStr(config.GroupSearchMode, implementation.GroupSearchMode) { + config.GroupSearchMode = implementation.GroupSearchMode + } + + if ldapImplementationShouldSetStr(config.Attributes.DistinguishedName, implementation.Attributes.DistinguishedName) { + config.Attributes.DistinguishedName = implementation.Attributes.DistinguishedName + } + + if ldapImplementationShouldSetStr(config.Attributes.Username, implementation.Attributes.Username) { + config.Attributes.Username = implementation.Attributes.Username + } + + if ldapImplementationShouldSetStr(config.Attributes.DisplayName, implementation.Attributes.DisplayName) { + config.Attributes.DisplayName = implementation.Attributes.DisplayName + } + + if ldapImplementationShouldSetStr(config.Attributes.Mail, implementation.Attributes.Mail) { + config.Attributes.Mail = implementation.Attributes.Mail + } + + if ldapImplementationShouldSetStr(config.Attributes.MemberOf, implementation.Attributes.MemberOf) { + config.Attributes.MemberOf = implementation.Attributes.MemberOf + } + + if ldapImplementationShouldSetStr(config.Attributes.GroupName, implementation.Attributes.GroupName) { + config.Attributes.GroupName = implementation.Attributes.GroupName } } @@ -486,4 +498,32 @@ func validateLDAPRequiredParameters(config *schema.AuthenticationBackend, valida } else if !strings.HasPrefix(config.LDAP.GroupsFilter, "(") || !strings.HasSuffix(config.LDAP.GroupsFilter, ")") { validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterEnclosingParenthesis, "groups_filter", config.LDAP.GroupsFilter, config.LDAP.GroupsFilter)) } + + validateLDAPGroupFilter(config, validator) +} + +func validateLDAPGroupFilter(config *schema.AuthenticationBackend, validator *schema.StructValidator) { + if config.LDAP.GroupSearchMode == "" { + config.LDAP.GroupSearchMode = schema.LDAPGroupSearchModeFilter + } + + if !utils.IsStringInSlice(config.LDAP.GroupSearchMode, validLDAPGroupSearchModes) { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendOptionMustBeOneOf, "group_search_mode", strJoinOr(validLDAPGroupSearchModes), config.LDAP.GroupSearchMode)) + } + + pMemberOfDN, pMemberOfRDN := strings.Contains(config.LDAP.GroupsFilter, "{memberof:dn}"), strings.Contains(config.LDAP.GroupsFilter, "{memberof:rdn}") + + if config.LDAP.GroupSearchMode == schema.LDAPGroupSearchModeMemberOf { + if !pMemberOfDN && !pMemberOfRDN { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholderGroupSearchMode, "groups_filter", strJoinOr([]string{"{memberof:rdn}", "{memberof:dn}"}), config.LDAP.GroupSearchMode)) + } + } + + if pMemberOfDN && config.LDAP.Attributes.DistinguishedName == "" { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingAttribute, "distinguished_name", strJoinOr([]string{"{memberof:dn}"}))) + } + + if (pMemberOfDN || pMemberOfRDN) && config.LDAP.Attributes.MemberOf == "" { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingAttribute, "member_of", strJoinOr([]string{"{memberof:rdn}", "{memberof:dn}"}))) + } } diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index a606348ce..b329cef63 100644 --- a/internal/configuration/validator/authentication_test.go +++ b/internal/configuration/validator/authentication_test.go @@ -577,7 +577,7 @@ func (suite *LDAPAuthenticationBackendSuite) SetupTest() { suite.config.LDAP.User = testLDAPUser suite.config.LDAP.Password = testLDAPPassword suite.config.LDAP.BaseDN = testLDAPBaseDN - suite.config.LDAP.UsernameAttribute = "uid" + suite.config.LDAP.Attributes.Username = "uid" suite.config.LDAP.UsersFilter = "({username_attribute}={input})" suite.config.LDAP.GroupsFilter = "(cn={input})" } @@ -591,12 +591,12 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateCompleteConfigura func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementationAndUsernameAttribute() { suite.config.LDAP.Implementation = "" - suite.config.LDAP.UsernameAttribute = "" + suite.config.LDAP.Attributes.Username = "" ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Equal(schema.LDAPImplementationCustom, suite.config.LDAP.Implementation) - suite.Equal(suite.config.LDAP.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.UsernameAttribute) + suite.Equal(suite.config.LDAP.Attributes.Username, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.Attributes.Username) suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) } @@ -743,7 +743,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsersFilter() } func (suite *LDAPAuthenticationBackendSuite) TestShouldNotRaiseOnEmptyUsernameAttribute() { - suite.config.LDAP.UsernameAttribute = "" + suite.config.LDAP.Attributes.Username = "" ValidateAuthenticationBackend(&suite.config, suite.validator) @@ -793,7 +793,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttrib suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) - suite.Equal("cn", suite.config.LDAP.GroupNameAttribute) + suite.Equal("cn", suite.config.LDAP.Attributes.GroupName) } func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() { @@ -802,7 +802,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) - suite.Equal("mail", suite.config.LDAP.MailAttribute) + suite.Equal("mail", suite.config.LDAP.Attributes.Mail) } func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultDisplayNameAttribute() { @@ -811,7 +811,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultDisplayNameAttr suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) - suite.Equal("displayName", suite.config.LDAP.DisplayNameAttribute) + suite.Equal("displayName", suite.config.LDAP.Attributes.DisplayName) } func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval() { @@ -890,6 +890,64 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldNotAllowSSL30() { suite.EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: tls: option 'minimum_version' is invalid: minimum version is TLS1.0 but SSL3.0 was configured") } +func (suite *LDAPAuthenticationBackendSuite) TestShouldErrorOnBadSearchMode() { + suite.config.LDAP.GroupSearchMode = "memberOF" + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'group_search_mode' must be one of 'filter' or 'memberof' but it's configured as 'memberOF'") +} + +func (suite *LDAPAuthenticationBackendSuite) TestShouldNoErrorOnPlaceholderSearchMode() { + suite.config.LDAP.GroupSearchMode = memberof + suite.config.LDAP.GroupsFilter = filterMemberOfRDN + suite.config.LDAP.Attributes.MemberOf = memberOf + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Len(suite.validator.Warnings(), 0) + suite.Len(suite.validator.Errors(), 0) +} + +func (suite *LDAPAuthenticationBackendSuite) TestShouldErrorOnMissingPlaceholderSearchMode() { + suite.config.LDAP.GroupSearchMode = memberof + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'groups_filter' must contain one of the '{memberof:rdn}' or '{memberof:dn}' placeholders when using a group_search_mode of 'memberof' but they're absent") +} + +func (suite *LDAPAuthenticationBackendSuite) TestShouldErrorOnMissingDistinguishedNameDN() { + suite.config.LDAP.Attributes.DistinguishedName = "" + suite.config.LDAP.GroupsFilter = "(|({memberof:dn}))" + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 2) + + suite.EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: attributes: option 'distinguished_name' must be provided when using the '{memberof:dn}' placeholder but it's absent") + suite.EqualError(suite.validator.Errors()[1], "authentication_backend: ldap: attributes: option 'member_of' must be provided when using the '{memberof:rdn}' or '{memberof:dn}' placeholder but it's absent") +} + +func (suite *LDAPAuthenticationBackendSuite) TestShouldErrorOnMissingMemberOfRDN() { + suite.config.LDAP.Attributes.DistinguishedName = "" + suite.config.LDAP.GroupsFilter = filterMemberOfRDN + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Len(suite.validator.Warnings(), 0) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: attributes: option 'member_of' must be provided when using the '{memberof:rdn}' or '{memberof:dn}' placeholder but it's absent") +} + func (suite *LDAPAuthenticationBackendSuite) TestShouldNotAllowTLSVerMinGreaterThanVerMax() { suite.config.LDAP.TLS = &schema.TLSConfig{ MinimumVersion: schema.TLSVersion{Value: tls.VersionTLS13}, @@ -909,9 +967,7 @@ func TestLDAPAuthenticationBackend(t *testing.T) { } type ActiveDirectoryAuthenticationBackendSuite struct { - suite.Suite - config schema.AuthenticationBackend - validator *schema.StructValidator + LDAPImplementationSuite } func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() { @@ -932,81 +988,30 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirec suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout, - suite.config.LDAP.Timeout) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.EqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory) } func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() { suite.config.LDAP.Timeout = time.Second * 2 suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))" - suite.config.LDAP.UsernameAttribute = "cn" - suite.config.LDAP.MailAttribute = "userPrincipalName" - suite.config.LDAP.DisplayNameAttribute = "name" + suite.config.LDAP.Attributes.Username = "cn" + suite.config.LDAP.Attributes.Mail = "userPrincipalName" + suite.config.LDAP.Attributes.DisplayName = "name" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))" - suite.config.LDAP.GroupNameAttribute = "distinguishedName" + suite.config.LDAP.Attributes.GroupName = "distinguishedName" suite.config.LDAP.AdditionalUsersDN = "OU=test" suite.config.LDAP.AdditionalGroupsDN = "OU=grps" + suite.config.LDAP.Attributes.MemberOf = member + suite.config.LDAP.GroupSearchMode = memberof + suite.config.LDAP.Attributes.DistinguishedName = "objectGUID" ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout, - suite.config.LDAP.Timeout) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.NotEqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory) + + suite.Equal(member, suite.config.LDAP.Attributes.MemberOf) + suite.Equal("objectGUID", suite.config.LDAP.Attributes.DistinguishedName) + suite.Equal(memberof, suite.config.LDAP.GroupSearchMode) } func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldRaiseErrorOnInvalidURLWithHTTP() { @@ -1023,9 +1028,7 @@ func TestActiveDirectoryAuthenticationBackend(t *testing.T) { } type RFC2307bisAuthenticationBackendSuite struct { - suite.Suite - config schema.AuthenticationBackend - validator *schema.StructValidator + LDAPImplementationSuite } func (suite *RFC2307bisAuthenticationBackendSuite) SetupTest() { @@ -1046,78 +1049,29 @@ func (suite *RFC2307bisAuthenticationBackendSuite) TestShouldSetDefaults() { suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.Timeout, - suite.config.LDAP.Timeout) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.EqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis) } func (suite *RFC2307bisAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() { suite.config.LDAP.Timeout = time.Second * 2 suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectClass=Person))" - suite.config.LDAP.UsernameAttribute = "o" - suite.config.LDAP.MailAttribute = "Email" - suite.config.LDAP.DisplayNameAttribute = "Given" + suite.config.LDAP.Attributes.Username = "o" + suite.config.LDAP.Attributes.Mail = "Email" + suite.config.LDAP.Attributes.DisplayName = "Given" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixGroup)(objectClass=top))" - suite.config.LDAP.GroupNameAttribute = "gid" + suite.config.LDAP.Attributes.GroupName = "gid" + suite.config.LDAP.Attributes.MemberOf = member suite.config.LDAP.AdditionalUsersDN = "OU=users,OU=OpenLDAP" suite.config.LDAP.AdditionalGroupsDN = "OU=groups,OU=OpenLDAP" + suite.config.LDAP.GroupSearchMode = memberof ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.Timeout, - suite.config.LDAP.Timeout) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.Timeout, - suite.config.LDAP.Timeout) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.NotEqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationRFC2307bis) + + suite.Equal(member, suite.config.LDAP.Attributes.MemberOf) + suite.Equal("", suite.config.LDAP.Attributes.DistinguishedName) + suite.Equal(schema.LDAPGroupSearchModeMemberOf, suite.config.LDAP.GroupSearchMode) } func TestRFC2307bisAuthenticationBackend(t *testing.T) { @@ -1125,9 +1079,7 @@ func TestRFC2307bisAuthenticationBackend(t *testing.T) { } type FreeIPAAuthenticationBackendSuite struct { - suite.Suite - config schema.AuthenticationBackend - validator *schema.StructValidator + LDAPImplementationSuite } func (suite *FreeIPAAuthenticationBackendSuite) SetupTest() { @@ -1148,75 +1100,29 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldSetDefaults() { suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout, - suite.config.LDAP.Timeout) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.EqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA) } func (suite *FreeIPAAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() { suite.config.LDAP.Timeout = time.Second * 2 suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectClass=person)(!(nsAccountLock=TRUE)))" - suite.config.LDAP.UsernameAttribute = "dn" - suite.config.LDAP.MailAttribute = "email" - suite.config.LDAP.DisplayNameAttribute = "gecos" + suite.config.LDAP.Attributes.Username = "dn" + suite.config.LDAP.Attributes.Mail = "email" + suite.config.LDAP.Attributes.DisplayName = "gecos" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixgroup))" - suite.config.LDAP.GroupNameAttribute = "groupName" + suite.config.LDAP.GroupSearchMode = schema.LDAPGroupSearchModeMemberOf + suite.config.LDAP.Attributes.GroupName = "groupName" + suite.config.LDAP.Attributes.MemberOf = member suite.config.LDAP.AdditionalUsersDN = "OU=people" suite.config.LDAP.AdditionalGroupsDN = "OU=grp" ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout, - suite.config.LDAP.Timeout) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.NotEqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA) + + suite.Equal(member, suite.config.LDAP.Attributes.MemberOf) + suite.Equal("", suite.config.LDAP.Attributes.DistinguishedName) + suite.Equal(schema.LDAPGroupSearchModeMemberOf, suite.config.LDAP.GroupSearchMode) } func TestFreeIPAAuthenticationBackend(t *testing.T) { @@ -1224,9 +1130,7 @@ func TestFreeIPAAuthenticationBackend(t *testing.T) { } type LLDAPAuthenticationBackendSuite struct { - suite.Suite - config schema.AuthenticationBackend - validator *schema.StructValidator + LDAPImplementationSuite } func (suite *LLDAPAuthenticationBackendSuite) SetupTest() { @@ -1247,78 +1151,29 @@ func (suite *LLDAPAuthenticationBackendSuite) TestShouldSetDefaults() { suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout, - suite.config.LDAP.Timeout) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.EqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP) } func (suite *LLDAPAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() { suite.config.LDAP.Timeout = time.Second * 2 suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectClass=Person)(!(nsAccountLock=TRUE)))" - suite.config.LDAP.UsernameAttribute = "username" - suite.config.LDAP.MailAttribute = "m" - suite.config.LDAP.DisplayNameAttribute = "fn" + suite.config.LDAP.Attributes.Username = "username" + suite.config.LDAP.Attributes.Mail = "m" + suite.config.LDAP.Attributes.DisplayName = "fn" + suite.config.LDAP.Attributes.MemberOf = member suite.config.LDAP.GroupsFilter = "(&(member={dn})(!(objectClass=posixGroup)))" - suite.config.LDAP.GroupNameAttribute = "grpz" + suite.config.LDAP.Attributes.GroupName = "grpz" suite.config.LDAP.AdditionalUsersDN = "OU=no" suite.config.LDAP.AdditionalGroupsDN = "OU=yes" + suite.config.LDAP.GroupSearchMode = memberof ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout, - suite.config.LDAP.Timeout) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout, - suite.config.LDAP.Timeout) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.NotEqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP) + + suite.Equal(member, suite.config.LDAP.Attributes.MemberOf) + suite.Equal("", suite.config.LDAP.Attributes.DistinguishedName) + suite.Equal(schema.LDAPGroupSearchModeMemberOf, suite.config.LDAP.GroupSearchMode) } func TestLLDAPAuthenticationBackend(t *testing.T) { @@ -1326,9 +1181,7 @@ func TestLLDAPAuthenticationBackend(t *testing.T) { } type GLAuthAuthenticationBackendSuite struct { - suite.Suite - config schema.AuthenticationBackend - validator *schema.StructValidator + LDAPImplementationSuite } func (suite *GLAuthAuthenticationBackendSuite) SetupTest() { @@ -1349,80 +1202,80 @@ func (suite *GLAuthAuthenticationBackendSuite) TestShouldSetDefaults() { suite.Len(suite.validator.Warnings(), 0) suite.Len(suite.validator.Errors(), 0) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.Timeout, - suite.config.LDAP.Timeout) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.EqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth) } func (suite *GLAuthAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() { suite.config.LDAP.Timeout = time.Second * 2 suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectClass=Person)(!(accountStatus=inactive)))" - suite.config.LDAP.UsernameAttribute = "description" - suite.config.LDAP.MailAttribute = "sender" - suite.config.LDAP.DisplayNameAttribute = "given" + suite.config.LDAP.Attributes.Username = "description" + suite.config.LDAP.Attributes.Mail = "sender" + suite.config.LDAP.Attributes.DisplayName = "given" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixGroup))" - suite.config.LDAP.GroupNameAttribute = "grp" + suite.config.LDAP.Attributes.GroupName = "grp" suite.config.LDAP.AdditionalUsersDN = "OU=users,OU=GlAuth" suite.config.LDAP.AdditionalGroupsDN = "OU=groups,OU=GLAuth" + suite.config.LDAP.Attributes.MemberOf = member + suite.config.LDAP.GroupSearchMode = memberof ValidateAuthenticationBackend(&suite.config, suite.validator) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.Timeout, - suite.config.LDAP.Timeout) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.AdditionalUsersDN, - suite.config.LDAP.AdditionalUsersDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.AdditionalGroupsDN, - suite.config.LDAP.AdditionalGroupsDN) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.Timeout, - suite.config.LDAP.Timeout) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.UsersFilter, - suite.config.LDAP.UsersFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.UsernameAttribute, - suite.config.LDAP.UsernameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.DisplayNameAttribute, - suite.config.LDAP.DisplayNameAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.MailAttribute, - suite.config.LDAP.MailAttribute) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.GroupsFilter, - suite.config.LDAP.GroupsFilter) - suite.NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth.GroupNameAttribute, - suite.config.LDAP.GroupNameAttribute) + suite.NotEqualImplementationDefaults(schema.DefaultLDAPAuthenticationBackendConfigurationImplementationGLAuth) + + suite.Equal(member, suite.config.LDAP.Attributes.MemberOf) + suite.Equal("", suite.config.LDAP.Attributes.DistinguishedName) + suite.Equal(schema.LDAPGroupSearchModeMemberOf, suite.config.LDAP.GroupSearchMode) } func TestGLAuthAuthenticationBackend(t *testing.T) { suite.Run(t, new(GLAuthAuthenticationBackendSuite)) } + +type LDAPImplementationSuite struct { + suite.Suite + config schema.AuthenticationBackend + validator *schema.StructValidator +} + +func (suite *LDAPImplementationSuite) EqualImplementationDefaults(expected schema.LDAPAuthenticationBackend) { + suite.Equal(expected.Timeout, suite.config.LDAP.Timeout) + suite.Equal(expected.AdditionalUsersDN, suite.config.LDAP.AdditionalUsersDN) + suite.Equal(expected.AdditionalGroupsDN, suite.config.LDAP.AdditionalGroupsDN) + suite.Equal(expected.UsersFilter, suite.config.LDAP.UsersFilter) + suite.Equal(expected.GroupsFilter, suite.config.LDAP.GroupsFilter) + suite.Equal(expected.GroupSearchMode, suite.config.LDAP.GroupSearchMode) + + suite.Equal(expected.Attributes.DistinguishedName, suite.config.LDAP.Attributes.DistinguishedName) + suite.Equal(expected.Attributes.Username, suite.config.LDAP.Attributes.Username) + suite.Equal(expected.Attributes.DisplayName, suite.config.LDAP.Attributes.DisplayName) + suite.Equal(expected.Attributes.Mail, suite.config.LDAP.Attributes.Mail) + suite.Equal(expected.Attributes.MemberOf, suite.config.LDAP.Attributes.MemberOf) + suite.Equal(expected.Attributes.GroupName, suite.config.LDAP.Attributes.GroupName) +} + +func (suite *LDAPImplementationSuite) NotEqualImplementationDefaults(expected schema.LDAPAuthenticationBackend) { + suite.NotEqual(expected.Timeout, suite.config.LDAP.Timeout) + suite.NotEqual(expected.UsersFilter, suite.config.LDAP.UsersFilter) + suite.NotEqual(expected.GroupsFilter, suite.config.LDAP.GroupsFilter) + suite.NotEqual(expected.GroupSearchMode, suite.config.LDAP.GroupSearchMode) + suite.NotEqual(expected.Attributes.Username, suite.config.LDAP.Attributes.Username) + suite.NotEqual(expected.Attributes.DisplayName, suite.config.LDAP.Attributes.DisplayName) + suite.NotEqual(expected.Attributes.Mail, suite.config.LDAP.Attributes.Mail) + suite.NotEqual(expected.Attributes.GroupName, suite.config.LDAP.Attributes.GroupName) + + if expected.Attributes.DistinguishedName != "" { + suite.NotEqual(expected.Attributes.DistinguishedName, suite.config.LDAP.Attributes.DistinguishedName) + } + + if expected.AdditionalUsersDN != "" { + suite.NotEqual(expected.AdditionalUsersDN, suite.config.LDAP.AdditionalUsersDN) + } + + if expected.AdditionalGroupsDN != "" { + suite.NotEqual(expected.AdditionalGroupsDN, suite.config.LDAP.AdditionalGroupsDN) + } + + if expected.Attributes.MemberOf != "" { + suite.NotEqual(expected.Attributes.MemberOf, suite.config.LDAP.Attributes.MemberOf) + } +} diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 4da34125c..c60588c18 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -98,9 +98,9 @@ const ( errFmtLDAPAuthBackendUnauthenticatedBindWithPassword = "authentication_backend: ldap: option 'permit_unauthenticated_bind' can't be enabled when a password is specified" errFmtLDAPAuthBackendUnauthenticatedBindWithResetEnabled = "authentication_backend: ldap: option 'permit_unauthenticated_bind' can't be enabled when password reset is enabled" - errFmtLDAPAuthBackendMissingOption = "authentication_backend: ldap: option '%s' is required" - errFmtLDAPAuthBackendTLSConfigInvalid = "authentication_backend: ldap: tls: %w" - errFmtLDAPAuthBackendImplementation = "authentication_backend: ldap: option 'implementation' " + + errFmtLDAPAuthBackendMissingOption = "authentication_backend: ldap: option '%s' is required" + errFmtLDAPAuthBackendTLSConfigInvalid = "authentication_backend: ldap: tls: %w" + errFmtLDAPAuthBackendOptionMustBeOneOf = "authentication_backend: ldap: option '%s' " + errSuffixMustBeOneOf errFmtLDAPAuthBackendFilterReplacedPlaceholders = "authentication_backend: ldap: option " + "'%s' has an invalid placeholder: '%s' has been removed, please use '%s' instead" @@ -109,6 +109,10 @@ const ( "'%s' must contain enclosing parenthesis: '%s' should probably be '(%s)'" errFmtLDAPAuthBackendFilterMissingPlaceholder = "authentication_backend: ldap: option " + "'%s' must contain the placeholder '{%s}' but it's absent" + errFmtLDAPAuthBackendFilterMissingPlaceholderGroupSearchMode = "authentication_backend: ldap: option " + + "'%s' must contain one of the %s placeholders when using a group_search_mode of '%s' but they're absent" + errFmtLDAPAuthBackendFilterMissingAttribute = "authentication_backend: ldap: attributes: option " + + "'%s' must be provided when using the %s placeholder but it's absent" ) // TOTP Error constants. @@ -374,17 +378,6 @@ const ( operatorNotPattern = "not pattern" ) -var ( - validLDAPImplementations = []string{ - schema.LDAPImplementationCustom, - schema.LDAPImplementationActiveDirectory, - schema.LDAPImplementationRFC2307bis, - schema.LDAPImplementationFreeIPA, - schema.LDAPImplementationLLDAP, - schema.LDAPImplementationGLAuth, - } -) - const ( legacy = "legacy" authzImplementationLegacy = "Legacy" @@ -400,6 +393,22 @@ var ( validAuthzAuthnStrategies = []string{"CookieSession", "HeaderAuthorization", "HeaderProxyAuthorization", "HeaderAuthRequestProxyAuthorization", "HeaderLegacy"} ) +var ( + validLDAPImplementations = []string{ + schema.LDAPImplementationCustom, + schema.LDAPImplementationActiveDirectory, + schema.LDAPImplementationRFC2307bis, + schema.LDAPImplementationFreeIPA, + schema.LDAPImplementationLLDAP, + schema.LDAPImplementationGLAuth, + } + + validLDAPGroupSearchModes = []string{ + schema.LDAPGroupSearchModeFilter, + schema.LDAPGroupSearchModeMemberOf, + } +) + var ( validArgon2Variants = []string{"argon2id", "id", "argon2i", "i", "argon2d", "d"} validSHA2CryptVariants = []string{digestSHA256, digestSHA512} diff --git a/internal/configuration/validator/const_test.go b/internal/configuration/validator/const_test.go index c11f928aa..c913ee214 100644 --- a/internal/configuration/validator/const_test.go +++ b/internal/configuration/validator/const_test.go @@ -13,6 +13,11 @@ const ( testLDAPURL = "ldap://ldap" testLDAPUser = "user" testEncryptionKey = "a_not_so_secure_encryption_key" + + member = "member" + memberof = "memberof" + memberOf = "memberOf" + filterMemberOfRDN = "(|({memberof:rdn}))" ) const ( diff --git a/internal/suites/HighAvailability/configuration.yml b/internal/suites/HighAvailability/configuration.yml index 8aece0597..782c7def2 100644 --- a/internal/suites/HighAvailability/configuration.yml +++ b/internal/suites/HighAvailability/configuration.yml @@ -21,16 +21,19 @@ authentication_backend: ldap: address: 'ldap://openldap' base_dn: dc=example,dc=com - username_attribute: uid additional_users_dn: ou=users users_filter: (&({username_attribute}={input})(objectClass=person)) additional_groups_dn: ou=groups groups_filter: (&(member={dn})(objectClass=groupOfNames)) - group_name_attribute: cn - mail_attribute: mail - display_name_attribute: displayName user: cn=admin,dc=example,dc=com password: password + attributes: + distinguished_name: 'distinguishedName' + username: 'uid' + display_name: 'displayName' + mail: 'mail' + member_of: 'memberOf' + group_name: 'cn' access_control: default_policy: deny diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml index 1847f411e..9ed5aa3c1 100644 --- a/internal/suites/LDAP/configuration.yml +++ b/internal/suites/LDAP/configuration.yml @@ -22,16 +22,19 @@ authentication_backend: tls: skip_verify: true base_dn: dc=example,dc=com - username_attribute: uid additional_users_dn: ou=users users_filter: (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(objectClass=inetOrgPerson)) # yamllint disable-line rule:line-length additional_groups_dn: ou=groups groups_filter: (&(member={dn})(objectClass=groupOfNames)) - group_name_attribute: cn - mail_attribute: mail - display_name_attribute: displayName user: cn=pwmanager,dc=example,dc=com password: password + attributes: + distinguished_name: '' + username: 'uid' + display_name: 'displayName' + mail: 'mail' + member_of: 'memberOf' + group_name: 'cn' session: secret: unsecure_session_secret diff --git a/internal/suites/example/kube/authelia/configs/configuration.yml b/internal/suites/example/kube/authelia/configs/configuration.yml index edb8e04d7..18636193c 100644 --- a/internal/suites/example/kube/authelia/configs/configuration.yml +++ b/internal/suites/example/kube/authelia/configs/configuration.yml @@ -20,15 +20,18 @@ authentication_backend: tls: skip_verify: true base_dn: dc=example,dc=com - username_attribute: uid additional_users_dn: ou=users users_filter: (&({username_attribute}={input})(objectClass=person)) additional_groups_dn: ou=groups groups_filter: (&(member={dn})(objectClass=groupOfNames)) - group_name_attribute: cn - mail_attribute: mail - display_name_attribute: displayName user: cn=admin,dc=example,dc=com + attributes: + distinguished_name: '' + username: 'uid' + display_name: 'displayName' + mail: 'mail' + member_of: 'memberOf' + group_name: 'cn' access_control: default_policy: deny