diff --git a/BREAKING.md b/BREAKING.md index bc341323b..084fb33bd 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -8,7 +8,13 @@ breaking changes and about what you should do to overcome those changes. ## Breaking in v4.7.0 -`logs_level` configuration key has been renamed to `log_level`. +* `logs_level` configuration key has been renamed to `log_level`. +* `users_filter` was a search pattern for a given user with the `{0}` matcher replaced with the +actual username. In v4.7.0, `username_attribute` has been introduced. Consequently, the computed +user filter utilised by the LDAP search query is a combination of filters based on the +`username_attribute` and `users_filter`. `users_filter` now reduces the scope of users targeted by +the LDAP search query. For instance if `username_attribute` is set to `uid` and `users_filter` is +set to `(objectClass=person)` then the computed filter is `(&(uid=john)(objectClass=person))`. ## Breaking in v4.0.0 diff --git a/config.template.yml b/config.template.yml index dd063f53c..cd6db1f9b 100644 --- a/config.template.yml +++ b/config.template.yml @@ -56,7 +56,7 @@ duo_api: # and retrieve information such as email address and groups # users belong to. # -# There are two supported backends: `ldap` and `file`. +# There are two supported backends: 'ldap' and 'file'. authentication_backend: # LDAP backend configuration. # @@ -66,28 +66,48 @@ authentication_backend: ldap: # The url to the ldap server. Scheme can be ldap:// or ldaps:// url: ldap://127.0.0.1 + # Skip verifying the server certificate (to allow self-signed certificate). skip_verify: false + # The base dn for every entries base_dn: dc=example,dc=com + + # The attribute holding the username of the user (introduced to handle + # case insensitive search queries: #561). + # Microsoft Active Directory usually uses 'sAMAccountName' + # OpenLDAP usually uses 'uid' + username_attribute: uid + # An additional dn to define the scope to all users additional_users_dn: ou=users - # The users filter used to find the user DN - # {0} is a matcher replaced by username. - # 'cn={0}' by default. - users_filter: (cn={0}) + + # This attribute is optional. The user filter used in the LDAP search queries + # is a combination of this filter and the username attribute. + # This filter is used to reduce the scope of users targeted by the LDAP search query. + # For instance, if the username attribute is set to 'uid', the computed filter is + # (&(uid=)(objectClass=person)) + # Recommended settings are as follows: + # Microsoft Active Directory '(&(objectCategory=person)(objectClass=user))' + # OpenLDAP '(objectClass=person)' or '(objectClass=inetOrgPerson)' + users_filter: (objectClass=person) + # An additional dn to define the scope of groups additional_groups_dn: ou=groups + # The groups filter used for retrieving groups of a given user. - # {0} is a matcher replaced by username. + # {0} is a matcher replaced by username (as provided in login portal). + # {1} is a matcher replaced by username (as stored in LDAP). # {dn} is a matcher replaced by user DN. - # {uid} is a matcher replaced by user uid. # 'member={dn}' by default. 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 mail_attribute: mail + # The username and password of the admin user. user: cn=admin,dc=example,dc=com # This secret can also be set using the env variables AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD @@ -119,7 +139,7 @@ authentication_backend: # Access control is a list of rules defining the authorizations applied for one # resource to users or group of users. # -# If 'access_control' is not defined, ACL rules are disabled and the `bypass` +# If 'access_control' is not defined, ACL rules are disabled and the 'bypass' # rule is applied, i.e., access is allowed to anyone. Otherwise restrictions follow # the rules defined. # @@ -129,27 +149,27 @@ authentication_backend: # Note: You must put patterns containing wildcards between simple quotes for the YAML # to be syntactically correct. # -# Definition: A `rule` is an object with the following keys: `domain`, `subject`, -# `policy` and `resources`. +# Definition: A 'rule' is an object with the following keys: 'domain', 'subject', +# 'policy' and 'resources'. # -# - `domain` defines which domain or set of domains the rule applies to. +# - 'domain' defines which domain or set of domains the rule applies to. # -# - `subject` defines the subject to apply authorizations to. This parameter is +# - 'subject' defines the subject to apply authorizations to. This parameter is # optional and matching any user if not provided. If provided, the parameter # represents either a user or a group. It should be of the form 'user:' # or 'group:'. # -# - `policy` is the policy to apply to resources. It must be either `bypass`, -# `one_factor`, `two_factor` or `deny`. +# - 'policy' is the policy to apply to resources. It must be either 'bypass', +# 'one_factor', 'two_factor' or 'deny'. # -# - `resources` is a list of regular expressions that matches a set of resources to +# - 'resources' is a list of regular expressions that matches a set of resources to # apply the policy to. This parameter is optional and matches any resource if not # provided. # # Note: the order of the rules is important. The first policy matching # (domain, resource, subject) applies. access_control: - # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`. + # Default policy can either be 'bypass', 'one_factor', 'two_factor' or 'deny'. # It is the policy applied to any resource if there is no policy to be applied # to the user. default_policy: deny @@ -251,7 +271,7 @@ regulation: max_retries: 3 # The time range during which the user can attempt login before being banned. - # The user is banned if the authentication failed `max_retries` times in a `find_time` seconds window. + # The user is banned if the authentication failed 'max_retries' times in a 'find_time' seconds window. find_time: 120 # The length of time before a banned user can login again. diff --git a/docs/configuration/authentication/ldap.md b/docs/configuration/authentication/ldap.md index 8c5ea8ef5..757e155ec 100644 --- a/docs/configuration/authentication/ldap.md +++ b/docs/configuration/authentication/ldap.md @@ -25,22 +25,33 @@ authentication_backend: # The base dn for every entries base_dn: dc=example,dc=com + + # The attribute holding the username of the user (introduced to handle + # case insensitive search queries: #561). + # Microsoft Active Directory usually uses 'sAMAccountName' + # OpenLDAP usually uses 'uid' + username_attribute: uid # An additional dn to define the scope to all users additional_users_dn: ou=users - # The users filter used to find the user DN - # {0} is a matcher replaced by username. - # 'cn={0}' by default. - users_filter: (cn={0}) + # This attribute is optional. The user filter used in the LDAP search queries + # is a combination of this filter and the username attribute. + # This filter is used to reduce the scope of users targeted by the LDAP search query. + # For instance, if the username attribute is set to 'uid', the computed filter is + # (&(uid=)(objectClass=person)) + # Recommended settings are as follows: + # Microsoft Active Directory '(&(objectCategory=person)(objectClass=user))' + # OpenLDAP '(objectClass=person)' or '(objectClass=inetOrgPerson)' + users_filter: (objectClass=person) # An additional dn to define the scope of groups additional_groups_dn: ou=groups # The groups filter used for retrieving groups of a given user. - # {0} is a matcher replaced by username. + # {0} is a matcher replaced by username (as provided in login portal). + # {1} is a matcher replaced by username (as stored in LDAP). # {dn} is a matcher replaced by user DN. - # {uid} is a matcher replaced by user uid. # 'member={dn}' by default. groups_filter: (&(member={dn})(objectclass=groupOfNames)) diff --git a/internal/authentication/file_user_provider.go b/internal/authentication/file_user_provider.go index 1c6c47094..bfd59c96a 100644 --- a/internal/authentication/file_user_provider.go +++ b/internal/authentication/file_user_provider.go @@ -101,8 +101,9 @@ func (p *FileUserProvider) CheckUserPassword(username string, password string) ( func (p *FileUserProvider) GetDetails(username string) (*UserDetails, error) { if details, ok := p.database.Users[username]; ok { return &UserDetails{ - Groups: details.Groups, - Emails: []string{details.Email}, + Username: username, + Groups: details.Groups, + Emails: []string{details.Email}, }, nil } return nil, fmt.Errorf("User '%s' does not exist in database", username) diff --git a/internal/authentication/file_user_provider_test.go b/internal/authentication/file_user_provider_test.go index 09fef413f..e6608922d 100644 --- a/internal/authentication/file_user_provider_test.go +++ b/internal/authentication/file_user_provider_test.go @@ -85,6 +85,7 @@ func TestShouldRetrieveUserDetails(t *testing.T) { provider := NewFileUserProvider(&config) details, err := provider.GetDetails("john") assert.NoError(t, err) + assert.Equal(t, details.Username, "john") assert.Equal(t, details.Emails, []string{"john.doe@authelia.com"}) assert.Equal(t, details.Groups, []string{"admins", "dev"}) }) diff --git a/internal/authentication/ldap_user_provider.go b/internal/authentication/ldap_user_provider.go index a9d020361..4c9db6480 100644 --- a/internal/authentication/ldap_user_provider.go +++ b/internal/authentication/ldap_user_provider.go @@ -75,12 +75,12 @@ func (p *LDAPUserProvider) CheckUserPassword(username string, password string) ( } defer adminClient.Close() - userDN, err := p.getUserDN(adminClient, username) + profile, err := p.getUserProfile(adminClient, username) if err != nil { return false, err } - conn, err := p.connect(userDN, password) + conn, err := p.connect(profile.DN, password) if err != nil { return false, fmt.Errorf("Authentication of user %s failed. Cause: %s", username, err) } @@ -100,85 +100,82 @@ func (p *LDAPUserProvider) ldapEscape(input string) string { return input } -func (p *LDAPUserProvider) getUserAttribute(conn LDAPConnection, username string, attribute string) ([]string, error) { - client, err := p.connect(p.configuration.User, p.configuration.Password) - if err != nil { - return nil, err - } - defer client.Close() +type ldapUserProfile struct { + DN string + Emails []string + Username string +} +func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, username string) (*ldapUserProfile, error) { username = p.ldapEscape(username) - userFilter := strings.Replace(p.configuration.UsersFilter, "{0}", username, -1) + userFilter := fmt.Sprintf("(%s=%s)", p.configuration.UsernameAttribute, username) + if p.configuration.UsersFilter != "" { + userFilter = fmt.Sprintf("(&%s%s)", userFilter, p.configuration.UsersFilter) + } baseDN := p.configuration.BaseDN if p.configuration.AdditionalUsersDN != "" { baseDN = p.configuration.AdditionalUsersDN + "," + baseDN } + attributes := []string{"dn", + p.configuration.MailAttribute, + p.configuration.UsernameAttribute} + // Search for the given username searchRequest := ldap.NewSearchRequest( baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, - 1, 0, false, userFilter, []string{attribute}, nil, + 1, 0, false, userFilter, attributes, nil, ) - sr, err := client.Search(searchRequest) + sr, err := conn.Search(searchRequest) if err != nil { return nil, fmt.Errorf("Cannot find user DN of user %s. Cause: %s", username, err) } - if len(sr.Entries) != 1 { - return nil, fmt.Errorf("No %s found for user %s", attribute, username) + if len(sr.Entries) == 0 { + return nil, fmt.Errorf("No user %s found", username) } - if attribute == "dn" { - return []string{sr.Entries[0].DN}, nil + if len(sr.Entries) > 1 { + return nil, fmt.Errorf("Multiple users %s found", username) } - return sr.Entries[0].Attributes[0].Values, nil -} - -func (p *LDAPUserProvider) getUserDN(conn LDAPConnection, username string) (string, error) { - values, err := p.getUserAttribute(conn, username, "dn") - - if err != nil { - return "", err + userProfile := ldapUserProfile{ + DN: sr.Entries[0].DN, + } + for _, attr := range sr.Entries[0].Attributes { + if attr.Name == p.configuration.MailAttribute { + userProfile.Emails = attr.Values + } else if attr.Name == p.configuration.UsernameAttribute { + if len(attr.Values) != 1 { + return nil, fmt.Errorf("User %s cannot have multiple value for attribute %s", username, p.configuration.UsernameAttribute) + } + userProfile.Username = attr.Values[0] + } } - if len(values) != 1 { - return "", fmt.Errorf("DN attribute of user %s must be set", username) + if userProfile.DN == "" { + return nil, fmt.Errorf("No DN has been found for user %s", username) } - return values[0], nil -} - -func (p *LDAPUserProvider) getUserUID(conn LDAPConnection, username string) (string, error) { - values, err := p.getUserAttribute(conn, username, "uid") - - if err != nil { - return "", err - } - - if len(values) != 1 { - return "", fmt.Errorf("UID attribute of user %s must be set", username) - } - - return values[0], nil + return &userProfile, nil } func (p *LDAPUserProvider) createGroupsFilter(conn LDAPConnection, username string) (string, error) { if strings.Contains(p.configuration.GroupsFilter, "{0}") { return strings.Replace(p.configuration.GroupsFilter, "{0}", username, -1), nil } else if strings.Contains(p.configuration.GroupsFilter, "{dn}") { - userDN, err := p.getUserDN(conn, username) + profile, err := p.getUserProfile(conn, username) if err != nil { return "", err } - return strings.Replace(p.configuration.GroupsFilter, "{dn}", userDN, -1), nil - } else if strings.Contains(p.configuration.GroupsFilter, "{uid}") { - userUID, err := p.getUserUID(conn, username) + return strings.Replace(p.configuration.GroupsFilter, "{dn}", profile.DN, -1), nil + } else if strings.Contains(p.configuration.GroupsFilter, "{1}") { + profile, err := p.getUserProfile(conn, username) if err != nil { return "", err } - return strings.Replace(p.configuration.GroupsFilter, "{uid}", userUID, -1), nil + return strings.Replace(p.configuration.GroupsFilter, "{1}", profile.Username, -1), nil } return p.configuration.GroupsFilter, nil } @@ -223,37 +220,15 @@ func (p *LDAPUserProvider) GetDetails(username string) (*UserDetails, error) { groups = append(groups, res.Attributes[0].Values...) } - userDN, err := p.getUserDN(conn, username) - + profile, err := p.getUserProfile(conn, username) if err != nil { return nil, err } - searchEmailRequest := ldap.NewSearchRequest( - userDN, ldap.ScopeBaseObject, ldap.NeverDerefAliases, - 0, 0, false, "(cn=*)", []string{p.configuration.MailAttribute}, nil, - ) - - sr, err = conn.Search(searchEmailRequest) - - if err != nil { - return nil, fmt.Errorf("Unable to retrieve email of user %s. Cause: %s", username, err) - } - - emails := make([]string, 0) - - for _, res := range sr.Entries { - if len(res.Attributes) == 0 { - logging.Logger().Warningf("No email retrieved from LDAP for user %s", username) - break - } - // append all values of the document. Normally there should be only one per document. - emails = append(emails, res.Attributes[0].Values...) - } - return &UserDetails{ - Emails: emails, - Groups: groups, + Username: profile.Username, + Emails: profile.Emails, + Groups: groups, }, nil } @@ -265,13 +240,13 @@ func (p *LDAPUserProvider) UpdatePassword(username string, newPassword string) e return fmt.Errorf("Unable to update password. Cause: %s", err) } - userDN, err := p.getUserDN(client, username) + profile, err := p.getUserProfile(client, username) if err != nil { return fmt.Errorf("Unable to update password. Cause: %s", err) } - modifyRequest := ldap.NewModifyRequest(userDN, nil) + modifyRequest := ldap.NewModifyRequest(profile.DN, nil) modifyRequest.Replace("userPassword", []string{newPassword}) diff --git a/internal/authentication/ldap_user_provider_test.go b/internal/authentication/ldap_user_provider_test.go index cabc60d6d..720c54085 100644 --- a/internal/authentication/ldap_user_provider_test.go +++ b/internal/authentication/ldap_user_provider_test.go @@ -110,29 +110,42 @@ func TestShouldEscapeUserInput(t *testing.T) { ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ URL: "ldap://127.0.0.1:389", User: "cn=admin,dc=example,dc=com", + UsernameAttribute: "uid", Password: "password", - UsersFilter: "uid={0}", AdditionalUsersDN: "ou=users", BaseDN: "dc=example,dc=com", }, mockFactory) - mockFactory.EXPECT(). - Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). - Return(mockConn, nil) - - mockConn.EXPECT(). - Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). - Return(nil) - - mockConn.EXPECT(). - Close() - mockConn.EXPECT(). // Here we ensure that the input has been correctly escaped. - Search(NewSearchRequestMatcher("uid=john\\=abc")). + Search(NewSearchRequestMatcher("(uid=john\\=abc)")). Return(&ldap.SearchResult{}, nil) - ldapClient.getUserAttribute(mockConn, "john=abc", "dn") + ldapClient.getUserProfile(mockConn, "john=abc") +} + +func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPConnectionFactory(ctrl) + mockConn := NewMockLDAPConnection(ctrl) + + ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + UsernameAttribute: "uid", + UsersFilter: "(&(objectCategory=person)(objectClass=user))", + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, mockFactory) + + mockConn.EXPECT(). + Search(NewSearchRequestMatcher("(&(uid=john)(&(objectCategory=person)(objectClass=user)))")). + Return(&ldap.SearchResult{}, nil) + + ldapClient.getUserProfile(mockConn, "john") } func createSearchResultWithAttributes(attributes ...*ldap.EntryAttribute) *ldap.SearchResult { @@ -160,6 +173,8 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { URL: "ldap://127.0.0.1:389", User: "cn=admin,dc=example,dc=com", Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", UsersFilter: "uid={0}", AdditionalUsersDN: "ou=users", BaseDN: "dc=example,dc=com", @@ -167,33 +182,46 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { mockFactory.EXPECT(). Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). - Return(mockConn, nil).Times(2) + Return(mockConn, nil) mockConn.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). - Return(nil). - Times(2) + Return(nil) mockConn.EXPECT(). - Close().Times(2) + Close() searchGroups := mockConn.EXPECT(). Search(gomock.Any()). Return(createSearchResultWithAttributes(), nil) - searchUserDN := mockConn.EXPECT(). + searchProfile := mockConn.EXPECT(). Search(gomock.Any()). - Return(createSearchResultWithAttributeValues("uid=john,dc=example,dc=com"), nil) - searchEmails := mockConn.EXPECT(). - Search(gomock.Any()). - Return(createSearchResultWithAttributeValues("test@example.com"), nil) + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + &ldap.Entry{ + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + &ldap.EntryAttribute{ + Name: "mail", + Values: []string{"test@example.com"}, + }, + &ldap.EntryAttribute{ + Name: "uid", + Values: []string{"john"}, + }, + }, + }, + }, + }, nil) - gomock.InOrder(searchGroups, searchUserDN, searchEmails) + gomock.InOrder(searchGroups, searchProfile) details, err := ldapClient.GetDetails("john") require.NoError(t, err) assert.ElementsMatch(t, details.Groups, []string{}) assert.ElementsMatch(t, details.Emails, []string{"test@example.com"}) + assert.Equal(t, details.Username, "john") } func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { @@ -207,6 +235,7 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { URL: "ldap://127.0.0.1:389", User: "cn=admin,dc=example,dc=com", Password: "password", + UsernameAttribute: "uid", UsersFilter: "uid={0}", AdditionalUsersDN: "ou=users", BaseDN: "dc=example,dc=com", @@ -214,31 +243,102 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { mockFactory.EXPECT(). Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). - Return(mockConn, nil).Times(2) + Return(mockConn, nil) mockConn.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). - Return(nil). - Times(2) + Return(nil) mockConn.EXPECT(). - Close().Times(2) + Close() searchGroups := mockConn.EXPECT(). Search(gomock.Any()). Return(createSearchResultWithAttributeValues("group1", "group2"), nil) - searchUserDN := mockConn.EXPECT(). + searchProfile := mockConn.EXPECT(). Search(gomock.Any()). - Return(createSearchResultWithAttributeValues("uid=john,dc=example,dc=com"), nil) - searchEmails := mockConn.EXPECT(). - Search(gomock.Any()). - Return(createSearchResultWithAttributes(), nil) + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + &ldap.Entry{ + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + &ldap.EntryAttribute{ + Name: "uid", + Values: []string{"john"}, + }, + }, + }, + }, + }, nil) - gomock.InOrder(searchGroups, searchUserDN, searchEmails) + gomock.InOrder(searchGroups, searchProfile) details, err := ldapClient.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 TestShouldReturnUsernameFromLDAP(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPConnectionFactory(ctrl) + mockConn := NewMockLDAPConnection(ctrl) + + ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + UsersFilter: "uid={0}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, mockFactory) + + mockFactory.EXPECT(). + Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). + Return(mockConn, nil) + + mockConn.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + mockConn.EXPECT(). + Close() + + searchGroups := mockConn.EXPECT(). + Search(gomock.Any()). + Return(createSearchResultWithAttributeValues("group1", "group2"), nil) + searchProfile := mockConn.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + &ldap.Entry{ + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + &ldap.EntryAttribute{ + Name: "mail", + Values: []string{"test@example.com"}, + }, + &ldap.EntryAttribute{ + Name: "uid", + Values: []string{"John"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(searchGroups, searchProfile) + + details, err := ldapClient.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.Username, "John") } diff --git a/internal/authentication/types.go b/internal/authentication/types.go index b4277cf1d..5fbfbe7af 100644 --- a/internal/authentication/types.go +++ b/internal/authentication/types.go @@ -2,6 +2,7 @@ package authentication // UserDetails represent the details retrieved for a given user. type UserDetails struct { - Emails []string - Groups []string + Username string + Emails []string + Groups []string } diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go index 6b6a85e2a..9418c0309 100644 --- a/internal/configuration/schema/authentication.go +++ b/internal/configuration/schema/authentication.go @@ -10,6 +10,7 @@ type LDAPAuthenticationBackendConfiguration struct { AdditionalGroupsDN string `mapstructure:"additional_groups_dn"` GroupsFilter string `mapstructure:"groups_filter"` GroupNameAttribute string `mapstructure:"group_name_attribute"` + UsernameAttribute string `mapstructure:"username_attribute"` MailAttribute string `mapstructure:"mail_attribute"` User string `mapstructure:"user"` Password string `mapstructure:"password"` diff --git a/internal/configuration/test_resources/config.yml b/internal/configuration/test_resources/config.yml index 833cadd15..67c89ad9f 100644 --- a/internal/configuration/test_resources/config.yml +++ b/internal/configuration/test_resources/config.yml @@ -19,8 +19,9 @@ 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: (cn={0}) + users_filter: (&(objectCategory=person)(objectClass=user)) additional_groups_dn: ou=groups groups_filter: (&(member={dn})(objectclass=groupOfNames)) group_name_attribute: cn diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index 7bc06e9c1..261cf6f21 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -121,19 +121,23 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB } if configuration.UsersFilter == "" { - configuration.UsersFilter = "(cn={0})" - } - - if !strings.HasPrefix(configuration.UsersFilter, "(") || !strings.HasSuffix(configuration.UsersFilter, ")") { - validator.Push(errors.New("The users filter should contain enclosing parenthesis. For instance cn={0} should be (cn={0})")) + validator.Push(errors.New("Please provide a users filter with `users_filter` attribute")) + } else { + if !strings.HasPrefix(configuration.UsersFilter, "(") || !strings.HasSuffix(configuration.UsersFilter, ")") { + validator.Push(errors.New("The users filter should contain enclosing parenthesis. For instance uid={0} should be (uid={0})")) + } } if configuration.GroupsFilter == "" { - configuration.GroupsFilter = "(member={dn})" + validator.Push(errors.New("Please provide a groups filter with `groups_filter` attribute")) + } else { + if !strings.HasPrefix(configuration.GroupsFilter, "(") || !strings.HasSuffix(configuration.GroupsFilter, ")") { + validator.Push(errors.New("The groups filter should contain enclosing parenthesis. For instance cn={0} should be (cn={0})")) + } } - if !strings.HasPrefix(configuration.GroupsFilter, "(") || !strings.HasSuffix(configuration.GroupsFilter, ")") { - validator.Push(errors.New("The groups filter should contain enclosing parenthesis. For instance cn={0} should be (cn={0})")) + if configuration.UsernameAttribute == "" { + validator.Push(errors.New("Please provide a username attribute with `username_attribute`")) } if configuration.GroupNameAttribute == "" { diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index a3333f172..8cdf982eb 100644 --- a/internal/configuration/validator/authentication_test.go +++ b/internal/configuration/validator/authentication_test.go @@ -169,6 +169,9 @@ func (suite *LdapAuthenticationBackendSuite) SetupTest() { suite.configuration.Ldap.User = "user" suite.configuration.Ldap.Password = "password" suite.configuration.Ldap.BaseDN = "base_dn" + suite.configuration.Ldap.UsernameAttribute = "uid" + suite.configuration.Ldap.UsersFilter = "(uid={0})" + suite.configuration.Ldap.GroupsFilter = "(cn={0})" } func (suite *LdapAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() { @@ -204,16 +207,20 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotPr assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a base DN to connect to the LDAP server") } -func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultUsersFilter() { +func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyFilterAndGroupsFilter() { + suite.configuration.Ldap.UsersFilter = "" + suite.configuration.Ldap.GroupsFilter = "" ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), "(cn={0})", suite.configuration.Ldap.UsersFilter) + require.Len(suite.T(), suite.validator.Errors(), 2) + assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a users filter with `users_filter` attribute") + assert.EqualError(suite.T(), suite.validator.Errors()[1], "Please provide a groups filter with `groups_filter` attribute") } -func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupsFilter() { +func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsernameAttribute() { + suite.configuration.Ldap.UsernameAttribute = "" ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), "(member={dn})", suite.configuration.Ldap.GroupsFilter) + require.Len(suite.T(), suite.validator.Errors(), 1) + assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a username attribute with `username_attribute`") } func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() { @@ -229,17 +236,17 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() { - suite.configuration.Ldap.UsersFilter = "cn={0}" + suite.configuration.Ldap.UsersFilter = "uid={0}" ValidateAuthenticationBackend(&suite.configuration, suite.validator) assert.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "The users filter should contain enclosing parenthesis. For instance cn={0} should be (cn={0})") + assert.EqualError(suite.T(), suite.validator.Errors()[0], "The users filter should contain enclosing parenthesis. For instance uid={0} should be (uid={0})") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenGroupsFilterDoesNotContainEnclosingParenthesis() { - suite.configuration.Ldap.UsersFilter = "cn={0}" + suite.configuration.Ldap.GroupsFilter = "cn={0}" ValidateAuthenticationBackend(&suite.configuration, suite.validator) assert.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "The users filter should contain enclosing parenthesis. For instance cn={0} should be (cn={0})") + assert.EqualError(suite.T(), suite.validator.Errors()[0], "The groups filter should contain enclosing parenthesis. For instance cn={0} should be (cn={0})") } func (suite *LdapAuthenticationBackendSuite) TestShouldAdaptLDAPURL() { diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go index 86a536111..00c11cb69 100644 --- a/internal/handlers/handler_firstfactor.go +++ b/internal/handlers/handler_firstfactor.go @@ -95,7 +95,7 @@ func FirstFactorPost(ctx *middlewares.AutheliaCtx) { // And set those information in the new session. userSession := ctx.GetSession() - userSession.Username = bodyJSON.Username + userSession.Username = userDetails.Username userSession.Groups = userDetails.Groups userSession.Emails = userDetails.Emails userSession.AuthenticationLevel = authentication.OneFactor diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go index df6780707..12265502e 100644 --- a/internal/handlers/handler_firstfactor_test.go +++ b/internal/handlers/handler_firstfactor_test.go @@ -163,8 +163,9 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() { EXPECT(). GetDetails(gomock.Eq("test")). Return(&authentication.UserDetails{ - Emails: []string{"test@example.com"}, - Groups: []string{"dev", "admins"}, + Username: "test", + Emails: []string{"test@example.com"}, + Groups: []string{"dev", "admins"}, }, nil) s.mock.StorageProviderMock. @@ -202,8 +203,9 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() { EXPECT(). GetDetails(gomock.Eq("test")). Return(&authentication.UserDetails{ - Emails: []string{"test@example.com"}, - Groups: []string{"dev", "admins"}, + Username: "test", + Emails: []string{"test@example.com"}, + Groups: []string{"dev", "admins"}, }, nil) s.mock.StorageProviderMock. @@ -231,6 +233,49 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() { assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups) } +func (s *FirstFactorSuite) TestShouldSaveUsernameFromAuthenticationBackendInSession() { + s.mock.UserProviderMock. + EXPECT(). + CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")). + Return(true, nil) + + s.mock.UserProviderMock. + EXPECT(). + GetDetails(gomock.Eq("test")). + Return(&authentication.UserDetails{ + // This is the name in authentication backend, in some setups the binding is + // case insensitive but the user ID in session must match the user in LDAP + // for the other modules of Authelia to be coherent. + Username: "Test", + Emails: []string{"test@example.com"}, + Groups: []string{"dev", "admins"}, + }, nil) + + s.mock.StorageProviderMock. + EXPECT(). + AppendAuthenticationLog(gomock.Any()). + Return(nil) + + s.mock.Ctx.Request.SetBodyString(`{ + "username": "test", + "password": "hello", + "keepMeLoggedIn": true + }`) + FirstFactorPost(s.mock.Ctx) + + // Respond with 200. + assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode()) + assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body()) + + // And store authentication in session. + session := s.mock.Ctx.GetSession() + assert.Equal(s.T(), "Test", session.Username) + assert.Equal(s.T(), true, session.KeepMeLoggedIn) + assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel) + assert.Equal(s.T(), []string{"test@example.com"}, session.Emails) + assert.Equal(s.T(), []string{"dev", "admins"}, session.Groups) +} + type FirstFactorRedirectionSuite struct { suite.Suite @@ -259,8 +304,9 @@ func (s *FirstFactorRedirectionSuite) SetupTest() { EXPECT(). GetDetails(gomock.Eq("test")). Return(&authentication.UserDetails{ - Emails: []string{"test@example.com"}, - Groups: []string{"dev", "admins"}, + Username: "test", + Emails: []string{"test@example.com"}, + Groups: []string{"dev", "admins"}, }, nil) s.mock.StorageProviderMock. diff --git a/internal/suites/HighAvailability/configuration.yml b/internal/suites/HighAvailability/configuration.yml index facf8edd7..606a8bbf9 100644 --- a/internal/suites/HighAvailability/configuration.yml +++ b/internal/suites/HighAvailability/configuration.yml @@ -2,117 +2,30 @@ # Authelia configuration # ############################################################### -# The port to listen on port: 9091 -# Log level -# -# Level of verbosity for logs log_level: debug jwt_secret: unsecure_secret -# TOTP Issuer Name -# -# This will be the issuer name displayed in Google Authenticator -# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names totp: issuer: authelia.com -# The authentication backend to use for verifying user passwords -# and retrieve information such as email address and groups -# users belong to. -# -# There are two supported backends: `ldap` and `file`. authentication_backend: - # LDAP backend configuration. - # - # This backend allows Authelia to be scaled to more - # than one instance and therefore is recommended for - # production. ldap: - # The url of the ldap server url: ldap://openldap - - # The base dn for every entries base_dn: dc=example,dc=com - - # An additional dn to define the scope to all users + username_attribute: uid additional_users_dn: ou=users - - # The users filter used to find the user DN - # {0} is a matcher replaced by username. - # 'cn={0}' by default. - users_filter: (cn={0}) - - # An additional dn to define the scope of groups + users_filter: (objectClass=person) additional_groups_dn: ou=groups - - # The groups filter used for retrieving groups of a given user. - # {0} is a matcher replaced by username. - # {dn} is a matcher replaced by user DN. - # 'member={dn}' by default. 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 mail_attribute: mail - - # The username and password of the admin user. user: cn=admin,dc=example,dc=com password: password - # File backend configuration. - # - # With this backend, the users database is stored in a file - # which is updated when users reset their passwords. - # Therefore, this backend is meant to be used in a dev environment - # and not in production since it prevents Authelia to be scaled to - # more than one instance. - # - ## file: - ## path: ./users_database.yml - -# Access Control -# -# Access control is a list of rules defining the authorizations applied for one -# resource to users or group of users. -# -# If 'access_control' is not defined, ACL rules are disabled and the `bypass` -# rule is applied, i.e., access is allowed to anyone. Otherwise restrictions follow -# the rules defined. -# -# Note: One can use the wildcard * to match any subdomain. -# It must stand at the beginning of the pattern. (example: *.mydomain.com) -# -# Note: You must put patterns containing wildcards between simple quotes for the YAML -# to be syntaxically correct. -# -# Definition: A `rule` is an object with the following keys: `domain`, `subject`, -# `policy` and `resources`. -# -# - `domain` defines which domain or set of domains the rule applies to. -# -# - `subject` defines the subject to apply authorizations to. This parameter is -# optional and matching any user if not provided. If provided, the parameter -# represents either a user or a group. It should be of the form 'user:' -# or 'group:'. -# -# - `policy` is the policy to apply to resources. It must be either `bypass`, -# `one_factor`, `two_factor` or `deny`. -# -# - `resources` is a list of regular expressions that matches a set of resources to -# apply the policy to. This parameter is optional and matches any resource if not -# provided. -# -# Note: the order of the rules is important. The first policy matching -# (domain, resource, subject) applies. access_control: - # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`. - # It is the policy applied to any resource if there is no policy to be applied - # to the user. default_policy: deny rules: @@ -162,55 +75,23 @@ access_control: subject: "user:bob" policy: two_factor -# Configuration of session cookies -# -# The session cookies identify the user once logged in. session: - # The name of the session cookie. (default: authelia_session). name: authelia_session - - # The secret to encrypt the session cookie. secret: unsecure_session_secret - - # The time in ms before the cookie expires and session is reset. expiration: 3600 # 1 hour - - # The inactivity time in ms before the session is reset. inactivity: 300 # 5 minutes - - # The domain to protect. - # Note: the authenticator must also be in that domain. If empty, the cookie - # is restricted to the subdomain of the issuer. domain: example.com - - # The redis connection details redis: host: redis port: 6379 password: authelia -# Configuration of the authentication regulation mechanism. -# -# This mechanism prevents attackers from brute forcing the first factor. -# It bans the user if too many attempts are done in a short period of -# time. regulation: - # The number of failed login attempts before user is banned. - # Set it to 0 to disable regulation. max_retries: 3 - - # The time range during which the user can attempt login before being banned. - # The user is banned if the authentication failed `max_retries` times in a `find_time` seconds window. find_time: 8 - - # The length of time before a banned user can login again. ban_time: 10 -# Configuration of the storage backend used to store data and secrets. -# -# You must use only an available configuration: local, sql storage: - # Settings to connect to mariadb server mysql: host: mariadb port: 3306 @@ -218,13 +99,7 @@ storage: username: admin password: password -# Configuration of the notification system. -# -# Notifications are sent to users when they require a password reset, a u2f -# registration or a TOTP registration. -# Use only an available configuration: filesystem, gmail notifier: - # Use a SMTP server for sending notifications smtp: host: smtp port: 1025 diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml index 6fb61a9eb..67d6b13b5 100644 --- a/internal/suites/LDAP/configuration.yml +++ b/internal/suites/LDAP/configuration.yml @@ -12,39 +12,16 @@ jwt_secret: very_important_secret authentication_backend: ldap: - # The url of the ldap server url: ldaps://openldap - - # Skip certificate verification (for self-signed certificates) skip_verify: true - - # The base dn for every entries base_dn: dc=example,dc=com - - # An additional dn to define the scope to all users + username_attribute: uid additional_users_dn: ou=users - - # The users filter used to find the user DN - # {0} is a matcher replaced by username. - # 'cn={0}' by default. - users_filter: (cn={0}) - - # An additional dn to define the scope of groups + users_filter: (objectClass=person) additional_groups_dn: ou=groups - - # The groups filter used for retrieving groups of a given user. - # {0} is a matcher replaced by username. - # {dn} is a matcher replaced by user DN. - # 'member={dn}' by default. 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 mail_attribute: mail - - # The username and password of the admin user. user: cn=admin,dc=example,dc=com password: password @@ -54,15 +31,10 @@ session: expiration: 3600 # 1 hour inactivity: 300 # 5 minutes -# Configuration of the storage backend used to store data and secrets. i.e. totp data storage: local: path: /var/lib/authelia/db.sqlite3 -# TOTP Issuer Name -# -# This will be the issuer name displayed in Google Authenticator -# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names totp: issuer: example.com @@ -78,19 +50,12 @@ access_control: - domain: "singlefactor.example.com" policy: one_factor -# Configuration of the authentication regulation mechanism. regulation: - # Set it to 0 to disable max_retries. max_retries: 3 - - # The user is banned if the authentication failed `max_retries` times in a `find_time` seconds window. find_time: 300 - - # The length of time before a banned user can login again. ban_time: 900 notifier: - # Use a SMTP server for sending notifications smtp: host: smtp port: 1025 diff --git a/internal/suites/example/compose/ldap/docker-compose.yml b/internal/suites/example/compose/ldap/docker-compose.yml index 1d94f45b0..e7413dfd4 100644 --- a/internal/suites/example/compose/ldap/docker-compose.yml +++ b/internal/suites/example/compose/ldap/docker-compose.yml @@ -16,5 +16,7 @@ services: - './example/compose/ldap/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom' command: - '--copy-service' + - '--loglevel' + - 'debug' networks: - authelianet \ No newline at end of file diff --git a/internal/suites/example/compose/ldap/ldif/base.ldif b/internal/suites/example/compose/ldap/ldif/base.ldif index 3d78917dd..aa2d95746 100644 --- a/internal/suites/example/compose/ldap/ldif/base.ldif +++ b/internal/suites/example/compose/ldap/ldif/base.ldif @@ -10,18 +10,19 @@ ou: users dn: cn=dev,ou=groups,dc=example,dc=com cn: dev -member: cn=john,ou=users,dc=example,dc=com -member: cn=bob,ou=users,dc=example,dc=com +member: uid=john,ou=users,dc=example,dc=com +member: uid=bob,ou=users,dc=example,dc=com objectclass: groupOfNames objectclass: top dn: cn=admins,ou=groups,dc=example,dc=com cn: admins -member: cn=john,ou=users,dc=example,dc=com +member: uid=john,ou=users,dc=example,dc=com objectclass: groupOfNames objectclass: top -dn: cn=john,ou=users,dc=example,dc=com +dn: uid=john,ou=users,dc=example,dc=com +uid: john cn: john objectclass: inetOrgPerson objectclass: top @@ -29,7 +30,8 @@ mail: john.doe@authelia.com sn: John Doe userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ -dn: cn=harry,ou=users,dc=example,dc=com +dn: uid=harry,ou=users,dc=example,dc=com +uid: harry cn: harry objectclass: inetOrgPerson objectclass: top @@ -37,7 +39,8 @@ mail: harry.potter@authelia.com sn: Harry Potter userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ -dn: cn=bob,ou=users,dc=example,dc=com +dn: uid=bob,ou=users,dc=example,dc=com +uid: bob cn: bob objectclass: inetOrgPerson objectclass: top @@ -45,7 +48,8 @@ mail: bob.dylan@authelia.com sn: Bob Dylan userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ -dn: cn=james,ou=users,dc=example,dc=com +dn: uid=james,ou=users,dc=example,dc=com +uid: james cn: james objectclass: inetOrgPerson objectclass: top @@ -53,7 +57,8 @@ mail: james.dean@authelia.com sn: James Dean userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ -dn: cn=blackhat,ou=users,dc=example,dc=com +dn: uid=blackhat,ou=users,dc=example,dc=com +uid: blackhat cn: blackhat objectclass: inetOrgPerson objectclass: top diff --git a/internal/suites/example/kube/authelia/configs/configuration.yml b/internal/suites/example/kube/authelia/configs/configuration.yml index 3fa78e4e4..0ebcbb6ac 100644 --- a/internal/suites/example/kube/authelia/configs/configuration.yml +++ b/internal/suites/example/kube/authelia/configs/configuration.yml @@ -12,8 +12,9 @@ authentication_backend: url: ldaps://ldap-service skip_verify: true base_dn: dc=example,dc=com + username_attribute: uid additional_users_dn: ou=users - users_filter: (cn={0}) + users_filter: (objectClass=person) additional_groups_dn: ou=groups groups_filter: (&(member={dn})(objectclass=groupOfNames)) group_name_attribute: cn diff --git a/internal/suites/example/kube/ldap/base.ldif b/internal/suites/example/kube/ldap/base.ldif index 3d78917dd..aa2d95746 100644 --- a/internal/suites/example/kube/ldap/base.ldif +++ b/internal/suites/example/kube/ldap/base.ldif @@ -10,18 +10,19 @@ ou: users dn: cn=dev,ou=groups,dc=example,dc=com cn: dev -member: cn=john,ou=users,dc=example,dc=com -member: cn=bob,ou=users,dc=example,dc=com +member: uid=john,ou=users,dc=example,dc=com +member: uid=bob,ou=users,dc=example,dc=com objectclass: groupOfNames objectclass: top dn: cn=admins,ou=groups,dc=example,dc=com cn: admins -member: cn=john,ou=users,dc=example,dc=com +member: uid=john,ou=users,dc=example,dc=com objectclass: groupOfNames objectclass: top -dn: cn=john,ou=users,dc=example,dc=com +dn: uid=john,ou=users,dc=example,dc=com +uid: john cn: john objectclass: inetOrgPerson objectclass: top @@ -29,7 +30,8 @@ mail: john.doe@authelia.com sn: John Doe userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ -dn: cn=harry,ou=users,dc=example,dc=com +dn: uid=harry,ou=users,dc=example,dc=com +uid: harry cn: harry objectclass: inetOrgPerson objectclass: top @@ -37,7 +39,8 @@ mail: harry.potter@authelia.com sn: Harry Potter userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ -dn: cn=bob,ou=users,dc=example,dc=com +dn: uid=bob,ou=users,dc=example,dc=com +uid: bob cn: bob objectclass: inetOrgPerson objectclass: top @@ -45,7 +48,8 @@ mail: bob.dylan@authelia.com sn: Bob Dylan userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ -dn: cn=james,ou=users,dc=example,dc=com +dn: uid=james,ou=users,dc=example,dc=com +uid: james cn: james objectclass: inetOrgPerson objectclass: top @@ -53,7 +57,8 @@ mail: james.dean@authelia.com sn: James Dean userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/ -dn: cn=blackhat,ou=users,dc=example,dc=com +dn: uid=blackhat,ou=users,dc=example,dc=com +uid: blackhat cn: blackhat objectclass: inetOrgPerson objectclass: top diff --git a/internal/suites/suite_high_availability.go b/internal/suites/suite_high_availability.go index ec99b8206..e77842790 100644 --- a/internal/suites/suite_high_availability.go +++ b/internal/suites/suite_high_availability.go @@ -31,7 +31,7 @@ func init() { return waitUntilAutheliaBackendIsReady(haDockerEnvironment) } - onSetupTimeout := func() error { + displayAutheliaLogs := func() error { backendLogs, err := haDockerEnvironment.Logs("authelia-backend", nil) if err != nil { return err @@ -53,10 +53,11 @@ func init() { GlobalRegistry.Register(highAvailabilitySuiteName, Suite{ SetUp: setup, SetUpTimeout: 5 * time.Minute, - OnSetupTimeout: onSetupTimeout, + OnSetupTimeout: displayAutheliaLogs, TestTimeout: 5 * time.Minute, TearDown: teardown, TearDownTimeout: 2 * time.Minute, + OnError: displayAutheliaLogs, Description: `This suite is made to test Authelia in a *complete* environment, that is, with all components making Authelia highly available.`, }) diff --git a/internal/suites/suite_ldap.go b/internal/suites/suite_ldap.go index b92332772..8ecd04728 100644 --- a/internal/suites/suite_ldap.go +++ b/internal/suites/suite_ldap.go @@ -30,7 +30,7 @@ func init() { return waitUntilAutheliaBackendIsReady(dockerEnvironment) } - onSetupTimeout := func() error { + displayAutheliaLogs := func() error { backendLogs, err := dockerEnvironment.Logs("authelia-backend", nil) if err != nil { return err @@ -53,9 +53,10 @@ func init() { GlobalRegistry.Register(ldapSuiteName, Suite{ SetUp: setup, SetUpTimeout: 5 * time.Minute, - OnSetupTimeout: onSetupTimeout, + OnSetupTimeout: displayAutheliaLogs, TestTimeout: 1 * time.Minute, TearDown: teardown, TearDownTimeout: 2 * time.Minute, + OnError: displayAutheliaLogs, }) }