Skip to content

Commit

Permalink
feat(configuration): lldap implementation (#4498)
Browse files Browse the repository at this point in the history
This adds a lldap LDAP implementation which purely adds sane defaults for lldap. There are no functional differences just when the implementation option is set to 'lldap' sane defaults which should be sufficient for most use cases are set. See the documentation at https://www.authelia.com/r/ldap#defaults for more details.
  • Loading branch information
james-d-elliott committed Dec 21, 2022
1 parent d67554a commit 5b8b314
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 15 deletions.
1 change: 1 addition & 0 deletions config.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ authentication_backend:
## Acceptable options are as follows:
## - 'activedirectory' - for Microsoft Active Directory.
## - 'freeipa' - for FreeIPA.
## - 'lldap' - for lldap.
## - 'custom' - for custom specifications of attributes and filters.
## This currently defaults to 'custom' to maintain existing behaviour.
##
Expand Down
50 changes: 37 additions & 13 deletions docs/content/en/reference/guides/ldap.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,24 @@ Authelia primarily supports this method.

## Implementation Guide

There are currently two implementations, `custom`, `activedirectory`, and `freeipa`. The `activedirectory`
implementation must be used if you wish to allow users to change or reset their password as Active Directory
uses a custom attribute and mechanism for this. The long term intention of this is to have logical defaults for various
RFC implementations of LDAP.
The following implementations exist:

- `custom`:
- Not specific to any particular LDAP provider
- `activedirectory`:
- Specific configuration defaults for [Active Directory]
- Special implementation details:
- Includes a special encoding format required for changing passwords with [Active Directory]
- `freeipa`:
- Specific configuration defaults for [FreeIPA]
- No special implementation details
- `lldap`:
- Specific configuration defaults for [lldap]
- No special implementation details

[Active Directory]: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-domain-services
[FreeIPA]: https://www.freeipa.org/
[lldap]: https://github.com/nitnelave/lldap

### Filter replacements

Expand All @@ -60,15 +74,15 @@ search.

#### 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-epoch} | search | The current time formatted as a Unix epoch |
| {date-time:msft-nt-epoch} | search | The current time formatted as a Microsoft NT epoch which is used by some Microsoft Active Directory attributes |
| 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-epoch} | search | The current time formatted as a Unix epoch |
| {date-time:msft-nt-epoch} | search | The current time formatted as a Microsoft NT epoch which is used by some Microsoft [Active Directory] attributes |

#### Groups filter replacements

Expand All @@ -82,6 +96,14 @@ search.

The below tables describes the current attribute defaults for each implementation.

#### Search Base defaults

The following set defaults for the `additional_users_dn` and `additional_groups_dn` values.

| Implementation | Users | Groups |
|:--------------:|:---------:|:---------:|
| lldap | OU=people | OU=groups |

#### Attribute defaults

This table describes the attribute defaults for each implementation. i.e. the username_attribute is described by the
Expand All @@ -92,6 +114,7 @@ Username column.
| custom | N/A | displayName | mail | cn |
| activedirectory | sAMAccountName | displayName | mail | cn |
| freeipa | uid | displayName | mail | cn |
| lldap | uid | cn | mail | cn |

#### Filter defaults

Expand All @@ -113,6 +136,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:msft-nt-epoch}))) | (&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912))) |
| 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)) |

##### Microsoft Active Directory sAMAccountType

Expand Down
1 change: 1 addition & 0 deletions internal/configuration/config.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ authentication_backend:
## Acceptable options are as follows:
## - 'activedirectory' - for Microsoft Active Directory.
## - 'freeipa' - for FreeIPA.
## - 'lldap' - for lldap.
## - 'custom' - for custom specifications of attributes and filters.
## This currently defaults to 'custom' to maintain existing behaviour.
##
Expand Down
16 changes: 16 additions & 0 deletions internal/configuration/schema/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,19 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA = LDAPAut
MinimumVersion: TLSVersion{tls.VersionTLS12},
},
}

// 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,
TLS: &TLSConfig{
MinimumVersion: TLSVersion{tls.VersionTLS12},
},
}
3 changes: 3 additions & 0 deletions internal/configuration/schema/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ const (

// LDAPImplementationFreeIPA is the string for the FreeIPA LDAP implementation.
LDAPImplementationFreeIPA = "freeipa"

// LDAPImplementationLLDAP is the string for the lldap LDAP implementation.
LDAPImplementationLLDAP = "lldap"
)

// TOTP Algorithm.
Expand Down
10 changes: 10 additions & 0 deletions internal/configuration/validator/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ func validateLDAPAuthenticationBackend(config *schema.AuthenticationBackend, val
implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory
case schema.LDAPImplementationFreeIPA:
implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA
case schema.LDAPImplementationLLDAP:
implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP
default:
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendImplementation, config.LDAP.Implementation, strings.Join(validLDAPImplementations, "', '")))
}
Expand Down Expand Up @@ -383,6 +385,14 @@ func ldapImplementationShouldSetStr(config, implementation string) bool {
}

func setDefaultImplementationLDAPAuthenticationBackendProfileAttributes(config *schema.LDAPAuthenticationBackend, implementation *schema.LDAPAuthenticationBackend) {
if ldapImplementationShouldSetStr(config.AdditionalUsersDN, implementation.AdditionalUsersDN) {
config.AdditionalUsersDN = implementation.AdditionalUsersDN
}

if ldapImplementationShouldSetStr(config.AdditionalGroupsDN, implementation.AdditionalGroupsDN) {
config.AdditionalGroupsDN = implementation.AdditionalGroupsDN
}

if ldapImplementationShouldSetStr(config.UsersFilter, implementation.UsersFilter) {
config.UsersFilter = implementation.UsersFilter
}
Expand Down
132 changes: 131 additions & 1 deletion internal/configuration/validator/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementat
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1)

suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'implementation' is configured as 'masd' but must be one of the following values: 'custom', 'activedirectory', 'freeipa'")
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'implementation' is configured as 'masd' but must be one of the following values: 'custom', 'activedirectory', 'freeipa', 'lldap'")
}

func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() {
Expand Down Expand Up @@ -906,6 +906,12 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirec
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter,
suite.config.LDAP.UsersFilter)
Expand Down Expand Up @@ -934,12 +940,20 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefault
suite.config.LDAP.DisplayNameAttribute = "name"
suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))"
suite.config.LDAP.GroupNameAttribute = "distinguishedName"
suite.config.LDAP.AdditionalUsersDN = "OU=test"
suite.config.LDAP.AdditionalGroupsDN = "OU=grps"

ValidateAuthenticationBackend(&suite.config, suite.validator)

suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter,
suite.config.LDAP.UsersFilter)
Expand Down Expand Up @@ -1009,6 +1023,12 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldSetDefaults() {
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter,
suite.config.LDAP.UsersFilter)
Expand Down Expand Up @@ -1037,12 +1057,20 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotMa
suite.config.LDAP.DisplayNameAttribute = "gecos"
suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixgroup))"
suite.config.LDAP.GroupNameAttribute = "groupName"
suite.config.LDAP.AdditionalUsersDN = "OU=people"
suite.config.LDAP.AdditionalGroupsDN = "OU=grp"

ValidateAuthenticationBackend(&suite.config, suite.validator)

suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter,
suite.config.LDAP.UsersFilter)
Expand All @@ -1066,3 +1094,105 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotMa
func TestFreeIPAAuthenticationBackend(t *testing.T) {
suite.Run(t, new(FreeIPAAuthenticationBackendSuite))
}

type LLDAPAuthenticationBackendSuite struct {
suite.Suite
config schema.AuthenticationBackend
validator *schema.StructValidator
}

func (suite *LLDAPAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.config = schema.AuthenticationBackend{}
suite.config.LDAP = &schema.LDAPAuthenticationBackend{}
suite.config.LDAP.Implementation = schema.LDAPImplementationLLDAP
suite.config.LDAP.URL = testLDAPURL
suite.config.LDAP.User = testLDAPUser
suite.config.LDAP.Password = testLDAPPassword
suite.config.LDAP.BaseDN = testLDAPBaseDN
suite.config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.TLS
}

func (suite *LLDAPAuthenticationBackendSuite) TestShouldSetDefaults() {
ValidateAuthenticationBackend(&suite.config, suite.validator)

suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)

suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsersFilter,
suite.config.LDAP.UsersFilter)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsernameAttribute,
suite.config.LDAP.UsernameAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.DisplayNameAttribute,
suite.config.LDAP.DisplayNameAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.MailAttribute,
suite.config.LDAP.MailAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupsFilter,
suite.config.LDAP.GroupsFilter)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupNameAttribute,
suite.config.LDAP.GroupNameAttribute)
}

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 = "given"
suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixGroup))"
suite.config.LDAP.GroupNameAttribute = "grp"
suite.config.LDAP.AdditionalUsersDN = "OU=no"
suite.config.LDAP.AdditionalGroupsDN = "OU=yes"

ValidateAuthenticationBackend(&suite.config, suite.validator)

suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsersFilter,
suite.config.LDAP.UsersFilter)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsernameAttribute,
suite.config.LDAP.UsernameAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.DisplayNameAttribute,
suite.config.LDAP.DisplayNameAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.MailAttribute,
suite.config.LDAP.MailAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupsFilter,
suite.config.LDAP.GroupsFilter)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupNameAttribute,
suite.config.LDAP.GroupNameAttribute)
}

func TestLLDAPAuthenticationBackend(t *testing.T) {
suite.Run(t, new(LLDAPAuthenticationBackendSuite))
}
2 changes: 1 addition & 1 deletion internal/configuration/validator/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ const (
)

var (
validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA}
validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA, schema.LDAPImplementationLLDAP}
)

var (
Expand Down

0 comments on commit 5b8b314

Please sign in to comment.