feat(authentication): ldap users reset filter
This allows setting a specific users filter for password resets. Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>feat-ldap-reset-filter
parent
92cf5a186d
commit
099f4fa5e0
|
@ -421,6 +421,11 @@ authentication_backend:
|
|||
## (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))
|
||||
# users_filter: '(&({username_attribute}={input})(objectClass=person))'
|
||||
|
||||
## Defaults to the users_filter. This filter is used exclusively for password resets. So you can leverage it to
|
||||
## prevent users who must change their password from logging in, but if you set a separate filter here they could
|
||||
## theoretically still reset their password.
|
||||
# users_reset_filter: '(&({username_attribute}={input})(objectClass=person))'
|
||||
|
||||
## The additional_groups_dn is prefixed to base_dn and delimited by a comma when searching for groups.
|
||||
## i.e. with this set to OU=Groups and base_dn set to DC=a,DC=com; OU=Groups,DC=a,DC=com is searched for groups.
|
||||
# additional_groups_dn: 'ou=groups'
|
||||
|
|
|
@ -209,10 +209,18 @@ exactly which OU to get users from for either security or performance reasons. F
|
|||
default negating this requirement. Refer to the [filter defaults](../../reference/guides/ldap.md#filter-defaults) for
|
||||
more information.*
|
||||
|
||||
The LDAP filter to narrow down which users are valid. This is important to set correctly as to exclude disabled users.
|
||||
The default value is dependent on the [implementation](#implementation), refer to the
|
||||
The LDAP filter to determine users are valid. This is important to set correctly as to exclude disabled users. The
|
||||
default value is dependent on the [implementation](#implementation), refer to the
|
||||
[attribute defaults](../../reference/guides/ldap.md#attribute-defaults) for more information.
|
||||
|
||||
### users_reset_filter
|
||||
|
||||
{{< confkey type="string" required="no" >}}
|
||||
|
||||
The LDAP filter to narrow down which users are valid. This is important to set correctly as to exclude disabled users.
|
||||
The default value is the same as [users_filter](#usersfilter). This can be leveraged to allow users who cannot login
|
||||
due to restrictions in the [users_filter](#usersfilter) to still be able to reset their password.
|
||||
|
||||
### additional_groups_dn
|
||||
|
||||
{{< confkey type="string" required="no" >}}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -8,7 +8,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
|
@ -106,7 +106,7 @@ func (p *LDAPUserProvider) CheckUserPassword(username string, password string) (
|
|||
|
||||
defer client.Close()
|
||||
|
||||
if profile, err = p.getUserProfile(client, username); err != nil {
|
||||
if profile, err = p.getUserProfile(client, username, false); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
|
@ -132,7 +132,7 @@ func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, er
|
|||
|
||||
defer client.Close()
|
||||
|
||||
if profile, err = p.getUserProfile(client, username); err != nil {
|
||||
if profile, err = p.getUserProfile(client, username, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -165,7 +165,7 @@ func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error)
|
|||
|
||||
defer client.Close()
|
||||
|
||||
if profile, err = p.getUserProfile(client, username); err != nil {
|
||||
if profile, err = p.getUserProfile(client, username, true); err != nil {
|
||||
return fmt.Errorf("unable to update password. Cause: %w", err)
|
||||
}
|
||||
|
||||
|
@ -306,11 +306,11 @@ func (p *LDAPUserProvider) searchReferrals(request *ldap.SearchRequest, result *
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string) (profile *ldapUserProfile, err error) {
|
||||
func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string, reset bool) (profile *ldapUserProfile, err error) {
|
||||
// Search for the given username.
|
||||
request := ldap.NewSearchRequest(
|
||||
p.usersBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
||||
1, 0, false, p.resolveUsersFilter(username), p.usersAttributes, nil,
|
||||
1, 0, false, p.resolveUsersFilter(username, reset), p.usersAttributes, nil,
|
||||
)
|
||||
|
||||
p.log.
|
||||
|
@ -518,8 +518,12 @@ func (p *LDAPUserProvider) getUserGroupsRequestMemberOf(client LDAPClient, usern
|
|||
return groups, nil
|
||||
}
|
||||
|
||||
func (p *LDAPUserProvider) resolveUsersFilter(input string) (filter string) {
|
||||
func (p *LDAPUserProvider) resolveUsersFilter(input string, reset bool) (filter string) {
|
||||
if reset {
|
||||
filter = p.config.UsersResetFilter
|
||||
} else {
|
||||
filter = p.config.UsersFilter
|
||||
}
|
||||
|
||||
if p.usersFilterReplacementInput {
|
||||
// The {input} placeholder is replaced by the username input.
|
||||
|
|
|
@ -91,6 +91,16 @@ func (p *LDAPUserProvider) parseDynamicUsersConfiguration() {
|
|||
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)
|
||||
|
||||
if p.config.UsersResetFilter == "" {
|
||||
p.config.UsersResetFilter = p.config.UsersFilter
|
||||
} else {
|
||||
p.config.UsersResetFilter = strings.ReplaceAll(p.config.UsersResetFilter, ldapPlaceholderDistinguishedNameAttribute, p.config.Attributes.DistinguishedName)
|
||||
p.config.UsersResetFilter = strings.ReplaceAll(p.config.UsersResetFilter, ldapPlaceholderUsernameAttribute, p.config.Attributes.Username)
|
||||
p.config.UsersResetFilter = strings.ReplaceAll(p.config.UsersResetFilter, ldapPlaceholderDisplayNameAttribute, p.config.Attributes.DisplayName)
|
||||
p.config.UsersResetFilter = strings.ReplaceAll(p.config.UsersResetFilter, ldapPlaceholderMailAttribute, p.config.Attributes.Mail)
|
||||
p.config.UsersResetFilter = strings.ReplaceAll(p.config.UsersResetFilter, ldapPlaceholderMemberOfAttribute, p.config.Attributes.MemberOf)
|
||||
}
|
||||
|
||||
p.log.Tracef("Dynamically generated users filter is %s", p.config.UsersFilter)
|
||||
|
||||
if len(p.config.Attributes.Username) != 0 && !utils.IsStringInSlice(p.config.Attributes.Username, p.usersAttributes) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -757,7 +757,7 @@ func TestShouldEscapeUserInput(t *testing.T) {
|
|||
Search(NewSearchRequestMatcher("(|(uid=john\\=abc)(mail=john\\=abc))")).
|
||||
Return(&ldap.SearchResult{}, nil)
|
||||
|
||||
_, err := provider.getUserProfile(mockClient, "john=abc")
|
||||
_, err := provider.getUserProfile(mockClient, "john=abc", false)
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, "user not found")
|
||||
}
|
||||
|
@ -823,7 +823,7 @@ func TestShouldReturnEmailWhenAttributeSameAsUsername(t *testing.T) {
|
|||
client, err := provider.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile, err := provider.getUserProfile(client, "john@example.com")
|
||||
profile, err := provider.getUserProfile(client, "john@example.com", false)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, profile)
|
||||
|
@ -897,7 +897,7 @@ func TestShouldReturnUsernameAndBlankDisplayNameWhenAttributesTheSame(t *testing
|
|||
client, err := provider.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile, err := provider.getUserProfile(client, "john@example.com")
|
||||
profile, err := provider.getUserProfile(client, "john@example.com", false)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, profile)
|
||||
|
@ -975,7 +975,7 @@ func TestShouldReturnBlankEmailAndDisplayNameWhenAttrsLenZero(t *testing.T) {
|
|||
client, err := provider.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile, err := provider.getUserProfile(client, "john@example.com")
|
||||
profile, err := provider.getUserProfile(client, "john@example.com", false)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NotNil(t, profile)
|
||||
|
@ -1022,7 +1022,7 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) {
|
|||
Search(NewSearchRequestMatcher("(&(uid=john)(&(objectCategory=person)(objectClass=user)))")).
|
||||
Return(&ldap.SearchResult{}, nil)
|
||||
|
||||
_, err := provider.getUserProfile(mockClient, "john")
|
||||
_, err := provider.getUserProfile(mockClient, "john", false)
|
||||
require.Error(t, err)
|
||||
assert.EqualError(t, err, "user not found")
|
||||
}
|
||||
|
@ -3975,7 +3975,7 @@ func TestShouldReturnErrorWhenMultipleUsernameAttributes(t *testing.T) {
|
|||
client, err := provider.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile, err := provider.getUserProfile(client, "john")
|
||||
profile, err := provider.getUserProfile(client, "john", false)
|
||||
|
||||
assert.Nil(t, profile)
|
||||
assert.EqualError(t, err, "user 'john' has 2 values for for attribute 'uid' but the attribute must be a single value attribute")
|
||||
|
@ -4044,7 +4044,7 @@ func TestShouldReturnErrorWhenZeroUsernameAttributes(t *testing.T) {
|
|||
client, err := provider.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile, err := provider.getUserProfile(client, "john")
|
||||
profile, err := provider.getUserProfile(client, "john", false)
|
||||
|
||||
assert.Nil(t, profile)
|
||||
assert.EqualError(t, err, "user 'john' must have value for attribute 'uid'")
|
||||
|
@ -4109,7 +4109,7 @@ func TestShouldReturnErrorWhenUsernameAttributeNotReturned(t *testing.T) {
|
|||
client, err := provider.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile, err := provider.getUserProfile(client, "john")
|
||||
profile, err := provider.getUserProfile(client, "john", false)
|
||||
|
||||
assert.Nil(t, profile)
|
||||
assert.EqualError(t, err, "user 'john' must have value for attribute 'uid'")
|
||||
|
@ -4195,7 +4195,7 @@ func TestShouldReturnErrorWhenMultipleUsersFound(t *testing.T) {
|
|||
client, err := provider.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile, err := provider.getUserProfile(client, "john")
|
||||
profile, err := provider.getUserProfile(client, "john", false)
|
||||
|
||||
assert.Nil(t, profile)
|
||||
assert.EqualError(t, err, "there were 2 users found when searching for 'john' but there should only be 1")
|
||||
|
@ -4264,7 +4264,7 @@ func TestShouldReturnErrorWhenNoDN(t *testing.T) {
|
|||
client, err := provider.connect()
|
||||
assert.NoError(t, err)
|
||||
|
||||
profile, err := provider.getUserProfile(client, "john")
|
||||
profile, err := provider.getUserProfile(client, "john", false)
|
||||
|
||||
assert.Nil(t, profile)
|
||||
assert.EqualError(t, err, "user 'john' must have a distinguished name but the result returned an empty distinguished name")
|
||||
|
@ -4581,7 +4581,7 @@ func TestShouldParseDynamicConfiguration(t *testing.T) {
|
|||
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)(accountExpires>=133147241190000000)(accountExpires>=20221205142839.0Z)))", provider.resolveUsersFilter("test@example.com", false))
|
||||
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"}))
|
||||
}
|
||||
|
||||
|
|
|
@ -421,6 +421,11 @@ authentication_backend:
|
|||
## (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))
|
||||
# users_filter: '(&({username_attribute}={input})(objectClass=person))'
|
||||
|
||||
## Defaults to the users_filter. This filter is used exclusively for password resets. So you can leverage it to
|
||||
## prevent users who must change their password from logging in, but if you set a separate filter here they could
|
||||
## theoretically still reset their password.
|
||||
# users_reset_filter: '(&({username_attribute}={input})(objectClass=person))'
|
||||
|
||||
## The additional_groups_dn is prefixed to base_dn and delimited by a comma when searching for groups.
|
||||
## i.e. with this set to OU=Groups and base_dn set to DC=a,DC=com; OU=Groups,DC=a,DC=com is searched for groups.
|
||||
# additional_groups_dn: 'ou=groups'
|
||||
|
|
|
@ -105,6 +105,7 @@ type LDAPAuthenticationBackend struct {
|
|||
|
||||
AdditionalUsersDN string `koanf:"additional_users_dn"`
|
||||
UsersFilter string `koanf:"users_filter"`
|
||||
UsersResetFilter string `koanf:"users_reset_filter"`
|
||||
|
||||
AdditionalGroupsDN string `koanf:"additional_groups_dn"`
|
||||
GroupsFilter string `koanf:"groups_filter"`
|
||||
|
|
|
@ -116,6 +116,7 @@ var Keys = []string{
|
|||
"authentication_backend.ldap.base_dn",
|
||||
"authentication_backend.ldap.additional_users_dn",
|
||||
"authentication_backend.ldap.users_filter",
|
||||
"authentication_backend.ldap.users_reset_filter",
|
||||
"authentication_backend.ldap.additional_groups_dn",
|
||||
"authentication_backend.ldap.groups_filter",
|
||||
"authentication_backend.ldap.group_search_mode",
|
||||
|
|
|
@ -479,30 +479,45 @@ func validateLDAPRequiredParameters(config *schema.AuthenticationBackend, valida
|
|||
if config.LDAP.UsersFilter == "" {
|
||||
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "users_filter"))
|
||||
} else {
|
||||
if !strings.HasPrefix(config.LDAP.UsersFilter, "(") || !strings.HasSuffix(config.LDAP.UsersFilter, ")") {
|
||||
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterEnclosingParenthesis, "users_filter", config.LDAP.UsersFilter, config.LDAP.UsersFilter))
|
||||
validateLDAPUsersFilter(config, "users_filter", config.LDAP.UsersFilter, validator)
|
||||
}
|
||||
|
||||
if !strings.Contains(config.LDAP.UsersFilter, "{username_attribute}") {
|
||||
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholder, "users_filter", "username_attribute"))
|
||||
if config.LDAP.UsersResetFilter != "" {
|
||||
validateLDAPUsersFilter(config, "users_reset_filter", config.LDAP.UsersFilter, validator)
|
||||
}
|
||||
|
||||
// This test helps the user know that users_filter is broken after the breaking change induced by this commit.
|
||||
if !strings.Contains(config.LDAP.UsersFilter, "{input}") {
|
||||
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholder, "users_filter", "input"))
|
||||
}
|
||||
validateLDAPGroupFilter(config, validator)
|
||||
}
|
||||
|
||||
func validateLDAPUsersFilter(config *schema.AuthenticationBackend, name, filter string, val *schema.StructValidator) {
|
||||
if !strings.HasPrefix(filter, "(") || !strings.HasSuffix(filter, ")") {
|
||||
val.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterEnclosingParenthesis, name, filter, filter))
|
||||
}
|
||||
|
||||
if !strings.Contains(filter, "{username_attribute}") {
|
||||
val.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholder, name, "username_attribute"))
|
||||
}
|
||||
|
||||
if !strings.Contains(filter, "{input}") {
|
||||
val.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholder, name, "input"))
|
||||
}
|
||||
|
||||
if config.LDAP.Attributes.DistinguishedName == "" && strings.Contains(filter, "{distinguished_name_attribute}") {
|
||||
val.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingAttribute, "distinguished_name", strJoinOr([]string{"{distinguished_name_attribute}"})))
|
||||
}
|
||||
|
||||
if config.LDAP.Attributes.MemberOf == "" && strings.Contains(filter, "{member_of_attribute}") {
|
||||
val.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingAttribute, "member_of", strJoinOr([]string{"{member_of_attribute}"})))
|
||||
}
|
||||
}
|
||||
|
||||
func validateLDAPGroupFilter(config *schema.AuthenticationBackend, validator *schema.StructValidator) {
|
||||
if config.LDAP.GroupsFilter == "" {
|
||||
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "groups_filter"))
|
||||
} 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
|
||||
}
|
||||
|
@ -511,7 +526,7 @@ func validateLDAPGroupFilter(config *schema.AuthenticationBackend, validator *sc
|
|||
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}")
|
||||
pMemberOf, pMemberOfDN, pMemberOfRDN := strings.Contains(config.LDAP.GroupsFilter, "{member_of_attribute}"), strings.Contains(config.LDAP.GroupsFilter, "{memberof:dn}"), strings.Contains(config.LDAP.GroupsFilter, "{memberof:rdn}")
|
||||
|
||||
if config.LDAP.GroupSearchMode == schema.LDAPGroupSearchModeMemberOf {
|
||||
if !pMemberOfDN && !pMemberOfRDN {
|
||||
|
@ -523,7 +538,13 @@ func validateLDAPGroupFilter(config *schema.AuthenticationBackend, validator *sc
|
|||
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingAttribute, "distinguished_name", strJoinOr([]string{"{memberof:dn}"})))
|
||||
}
|
||||
|
||||
if (pMemberOfDN || pMemberOfRDN) && config.LDAP.Attributes.MemberOf == "" {
|
||||
if config.LDAP.Attributes.MemberOf == "" {
|
||||
if pMemberOfDN || pMemberOfRDN {
|
||||
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingAttribute, "member_of", strJoinOr([]string{"{memberof:rdn}", "{memberof:dn}"})))
|
||||
}
|
||||
|
||||
if pMemberOf {
|
||||
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingAttribute, "member_of", strJoinOr([]string{"{member_of_attribute}"})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue