summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/authentication/ldap_user_provider.go14
-rw-r--r--internal/authentication/ldap_user_provider_test.go253
-rw-r--r--internal/authentication/ldap_util.go2
-rw-r--r--internal/handlers/const.go1
-rw-r--r--internal/handlers/handler_change_password.go76
-rw-r--r--internal/handlers/handler_change_password_test.go73
-rw-r--r--internal/mocks/authelia_ctx.go49
7 files changed, 414 insertions, 54 deletions
diff --git a/internal/authentication/ldap_user_provider.go b/internal/authentication/ldap_user_provider.go
index 502dbe502..9480717a6 100644
--- a/internal/authentication/ldap_user_provider.go
+++ b/internal/authentication/ldap_user_provider.go
@@ -338,7 +338,7 @@ func (p *LDAPUserProvider) ChangePassword(username, oldPassword string, newPassw
userPasswordOk, err := p.CheckUserPassword(username, oldPassword)
if err != nil {
- errorCode := ldapGetErrorCode(err)
+ errorCode := getLDAPResultCode(err)
if errorCode == ldap.LDAPResultInvalidCredentials {
return ErrIncorrectPassword
} else {
@@ -385,24 +385,22 @@ func (p *LDAPUserProvider) ChangePassword(username, oldPassword string, newPassw
//TODO: Better inform users regarding password reuse/password history.
if err != nil {
- if errorCode := ldapGetErrorCode(err); errorCode != -1 {
+ if errorCode := getLDAPResultCode(err); errorCode != -1 {
switch errorCode {
case ldap.LDAPResultInvalidCredentials,
ldap.LDAPResultInappropriateAuthentication:
- return ErrIncorrectPassword
+ return fmt.Errorf("%w: %v", ErrIncorrectPassword, err)
case ldap.LDAPResultConstraintViolation,
ldap.LDAPResultObjectClassViolation,
ldap.ErrorEmptyPassword,
ldap.LDAPResultUnwillingToPerform:
- return ErrPasswordWeak
- case ldap.LDAPResultInsufficientAccessRights:
- return ErrOperationFailed
+ return fmt.Errorf("%w: %v", ErrPasswordWeak, err)
default:
- return ErrOperationFailed
+ return fmt.Errorf("%w: %v", ErrOperationFailed, err)
}
}
- return ErrOperationFailed
+ return fmt.Errorf("%w: %v", ErrOperationFailed, err)
}
return nil
diff --git a/internal/authentication/ldap_user_provider_test.go b/internal/authentication/ldap_user_provider_test.go
index 7b6b4ee73..5f2e13091 100644
--- a/internal/authentication/ldap_user_provider_test.go
+++ b/internal/authentication/ldap_user_provider_test.go
@@ -8,6 +8,8 @@ import (
"time"
"github.com/go-ldap/ldap/v3"
+ "github.com/sirupsen/logrus"
+ "github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
@@ -6520,3 +6522,254 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) {
_, err := provider.GetDetails("john")
assert.EqualError(t, err, "error occurred performing starttls: LDAP Result Code 200 \"Network Error\": ldap: already encrypted")
}
+
+func TestLDAPUserProviderChangePasswordErrors(t *testing.T) {
+ testCases := []struct {
+ name string
+ setupMocks func(ctrl *gomock.Controller) (*MockLDAPClientDialer, *MockLDAPClient)
+ username string
+ oldPassword string
+ newPassword string
+ expectedError error
+ expectedLogMsg string
+ expectedLogType logrus.Level
+ }{
+ {
+ name: "ShouldFailWhenClientError",
+ setupMocks: func(ctrl *gomock.Controller) (*MockLDAPClientDialer, *MockLDAPClient) {
+ mockDialer := NewMockLDAPClientDialer(ctrl)
+ mockClient := NewMockLDAPClient(ctrl)
+
+ mockDialer.EXPECT().DialURL("ldap://127.0.0.1:389", gomock.Any()).
+ Return(nil, errors.New("connection error"))
+
+ return mockDialer, mockClient
+ },
+ username: "john",
+ oldPassword: "oldpass",
+ newPassword: "newpass",
+ expectedError: fmt.Errorf("unable to update password for user 'john'. Cause: error occurred dialing address: connection error"),
+ expectedLogMsg: "",
+ expectedLogType: 0,
+ },
+ {
+ name: "ShouldFailWhenGetUserProfileError",
+ setupMocks: func(ctrl *gomock.Controller) (*MockLDAPClientDialer, *MockLDAPClient) {
+ mockDialer := NewMockLDAPClientDialer(ctrl)
+ mockClient := NewMockLDAPClient(ctrl)
+
+ mockDialer.EXPECT().DialURL("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, errors.New("search error"))
+
+ mockClient.EXPECT().Close()
+
+ return mockDialer, mockClient
+ },
+ username: "john",
+ oldPassword: "oldpass",
+ newPassword: "newpass",
+ expectedError: fmt.Errorf("unable to update password for user 'john'. Cause: cannot find user DN of user 'john'. Cause: search error"),
+ expectedLogMsg: "",
+ expectedLogType: 0,
+ },
+ {
+ name: "ShouldFailWithInvalidCredentials",
+ setupMocks: func(ctrl *gomock.Controller) (*MockLDAPClientDialer, *MockLDAPClient) {
+ mockDialer := NewMockLDAPClientDialer(ctrl)
+ mockClient := NewMockLDAPClient(ctrl)
+
+ mockDialer.EXPECT().DialURL("ldap://127.0.0.1:389", gomock.Any()).
+ Return(mockClient, nil)
+
+ mockDialer.EXPECT().DialURL("ldap://127.0.0.1:389", gomock.Any()).
+ Return(mockClient, nil)
+
+ mockDialer.EXPECT().DialURL("ldap://127.0.0.1:389", gomock.Any()).
+ Return(mockClient, nil)
+
+ mockClient.EXPECT().Bind("cn=admin,dc=example,dc=com", "password").
+ Return(nil)
+
+ mockClient.EXPECT().Bind("cn=admin,dc=example,dc=com", "password").
+ Return(nil)
+
+ mockClient.EXPECT().Search(gomock.Any()).
+ Return(&ldap.SearchResult{
+ Entries: []*ldap.Entry{
+ {
+ DN: "uid=john,ou=users,dc=example,dc=com",
+ Attributes: []*ldap.EntryAttribute{
+ {
+ Name: "uid",
+ Values: []string{"john"},
+ },
+ },
+ },
+ },
+ }, nil)
+
+ mockClient.EXPECT().Search(gomock.Any()).
+ Return(&ldap.SearchResult{
+ Entries: []*ldap.Entry{
+ {
+ DN: "uid=john,ou=users,dc=example,dc=com",
+ Attributes: []*ldap.EntryAttribute{
+ {
+ Name: "uid",
+ Values: []string{"john"},
+ },
+ },
+ },
+ },
+ }, nil)
+
+ mockClient.EXPECT().Bind("uid=john,ou=users,dc=example,dc=com", "oldpass").
+ Return(ldap.NewError(ldap.LDAPResultInvalidCredentials, errors.New("invalid credentials")))
+
+ mockClient.EXPECT().Close().Times(3)
+
+ return mockDialer, mockClient
+ },
+ username: "john",
+ oldPassword: "oldpass",
+ newPassword: "newpass",
+ expectedError: ErrIncorrectPassword,
+ expectedLogMsg: "",
+ expectedLogType: 0,
+ },
+ {
+ name: "ShouldFailWhenSamePassword",
+ setupMocks: func(ctrl *gomock.Controller) (*MockLDAPClientDialer, *MockLDAPClient) {
+ mockDialer := NewMockLDAPClientDialer(ctrl)
+ mockClient := NewMockLDAPClient(ctrl)
+
+ mockDialer.EXPECT().DialURL("ldap://127.0.0.1:389", gomock.Any()).
+ Return(mockClient, nil).Times(3)
+
+ mockClient.EXPECT().Bind("cn=admin,dc=example,dc=com", "password").
+ Return(nil).Times(2)
+
+ mockClient.EXPECT().Search(gomock.Any()).
+ Return(&ldap.SearchResult{
+ Entries: []*ldap.Entry{
+ {
+ DN: "uid=john,ou=users,dc=example,dc=com",
+ Attributes: []*ldap.EntryAttribute{
+ {Name: "uid", Values: []string{"john"}},
+ },
+ },
+ },
+ }, nil).Times(2)
+
+ mockClient.EXPECT().Bind("uid=john,ou=users,dc=example,dc=com", "samepass").
+ Return(nil)
+
+ mockClient.EXPECT().Close().Times(3)
+
+ return mockDialer, mockClient
+ },
+ username: "john",
+ oldPassword: "samepass",
+ newPassword: "samepass",
+ expectedError: ErrPasswordWeak,
+ expectedLogMsg: "",
+ expectedLogType: 0,
+ },
+ {
+ name: "ShouldFailOnModifyError",
+ setupMocks: func(ctrl *gomock.Controller) (*MockLDAPClientDialer, *MockLDAPClient) {
+ mockDialer := NewMockLDAPClientDialer(ctrl)
+ mockClient := NewMockLDAPClient(ctrl)
+
+ mockDialer.EXPECT().DialURL("ldap://127.0.0.1:389", gomock.Any()).
+ Return(mockClient, nil).Times(3)
+
+ mockClient.EXPECT().Bind("cn=admin,dc=example,dc=com", "password").
+ Return(nil).Times(2)
+
+ mockClient.EXPECT().Search(gomock.Any()).
+ Return(&ldap.SearchResult{
+ Entries: []*ldap.Entry{
+ {
+ DN: "uid=john,ou=users,dc=example,dc=com",
+ Attributes: []*ldap.EntryAttribute{
+ {Name: "uid", Values: []string{"john"}},
+ },
+ },
+ },
+ }, nil).Times(2)
+
+ mockClient.EXPECT().Bind("uid=john,ou=users,dc=example,dc=com", "oldpass").
+ Return(nil)
+
+ mockClient.EXPECT().Modify(gomock.Any()).
+ Return(ldap.NewError(ldap.LDAPResultConstraintViolation, errors.New("password too weak")))
+
+ mockClient.EXPECT().Close().Times(3)
+
+ return mockDialer, mockClient
+ },
+ username: "john",
+ oldPassword: "oldpass",
+ newPassword: "newpass",
+ expectedError: fmt.Errorf("your supplied password does not meet the password policy requirements: LDAP Result Code 19 \"Constraint Violation\": password too weak"),
+ expectedLogMsg: "",
+ expectedLogType: 0,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ // can't use mocks package due to circular import (mocks -> authentication, authentication(test) -> mocks).
+ logger := logrus.New()
+ hook := test.NewLocal(logger)
+ logger.SetLevel(logrus.TraceLevel)
+
+ mockDialer, _ := tc.setupMocks(ctrl)
+
+ config := &schema.AuthenticationBackendLDAP{
+ Address: testLDAPAddress,
+ User: "cn=admin,dc=example,dc=com",
+ Password: "password",
+ Attributes: schema.AuthenticationBackendLDAPAttributes{
+ Username: "uid",
+ Mail: "mail",
+ DisplayName: "displayName",
+ MemberOf: "memberOf",
+ },
+ UsersFilter: "uid={input}",
+ AdditionalUsersDN: "ou=users",
+ BaseDN: "dc=example,dc=com",
+ }
+
+ provider := NewLDAPUserProviderWithFactory(config, false, NewStandardLDAPClientFactory(config, nil, mockDialer))
+
+ err := provider.ChangePassword(tc.username, tc.oldPassword, tc.newPassword)
+
+ if tc.expectedError != nil {
+ assert.Error(t, err)
+ assert.Equal(t, tc.expectedError.Error(), err.Error())
+ } else {
+ assert.NoError(t, err)
+ }
+
+ if tc.expectedLogMsg != "" {
+ entry := hook.LastEntry()
+ require.NotNil(t, entry)
+ assert.Equal(t, tc.expectedLogType, logger.Level)
+ assert.Contains(t, entry.Message, tc.expectedLogMsg)
+ }
+ })
+ }
+}
diff --git a/internal/authentication/ldap_util.go b/internal/authentication/ldap_util.go
index 5793a661b..a50c69e2a 100644
--- a/internal/authentication/ldap_util.go
+++ b/internal/authentication/ldap_util.go
@@ -111,7 +111,7 @@ func ldapGetReferral(err error) (referral string, ok bool) {
}
}
-func ldapGetErrorCode(err error) int {
+func getLDAPResultCode(err error) int {
var e *ldap.Error
if errors.As(err, &e) {
diff --git a/internal/handlers/const.go b/internal/handlers/const.go
index 884095d5f..ac7a48523 100644
--- a/internal/handlers/const.go
+++ b/internal/handlers/const.go
@@ -69,7 +69,6 @@ const (
messageUnableToRegisterSecurityKey = "Unable to register your security key."
messageSecurityKeyDuplicateName = "Another one of your security keys is already registered with that display name."
messageUnableToResetPassword = "Unable to reset your password."
- messageCannotReusePassword = "You cannot reuse your old password."
messageUnableToChangePassword = "Unable to change your password."
messageIncorrectPassword = "Incorrect Password"
messageMFAValidationFailed = "Authentication failed, please retry later."
diff --git a/internal/handlers/handler_change_password.go b/internal/handlers/handler_change_password.go
index cbdd25ddc..cad4de1e5 100644
--- a/internal/handlers/handler_change_password.go
+++ b/internal/handlers/handler_change_password.go
@@ -2,7 +2,6 @@ package handlers
import (
"errors"
- "fmt"
"net/http"
"github.com/authelia/authelia/v4/internal/authentication"
@@ -14,11 +13,25 @@ import (
func ChangePasswordPOST(ctx *middlewares.AutheliaCtx) {
var (
userSession session.UserSession
+ provider *session.Session
err error
)
- if userSession, err = ctx.GetSession(); err != nil {
- ctx.Error(fmt.Errorf("error occurred retrieving session for user: %w", err), messageUnableToChangePassword)
+ if provider, err = ctx.GetSessionProvider(); err != nil {
+ ctx.Logger.WithError(err).
+ Error("Unable to change password for user: error occurred retrieving session provider")
+ ctx.SetJSONError(messageUnableToChangePassword)
+ ctx.SetStatusCode(http.StatusInternalServerError)
+
+ return
+ }
+
+ if userSession, err = provider.GetSession(ctx.RequestCtx); err != nil {
+ ctx.Logger.WithError(err).
+ Error("Unable to change password for user: error occurred retrieving session for user")
+ ctx.SetJSONError(messageUnableToChangePassword)
+ ctx.SetStatusCode(http.StatusInternalServerError)
+
return
}
@@ -27,29 +40,49 @@ func ChangePasswordPOST(ctx *middlewares.AutheliaCtx) {
var requestBody changePasswordRequestBody
if err = ctx.ParseBody(&requestBody); err != nil {
- ctx.Error(err, messageUnableToChangePassword)
+ ctx.Logger.WithError(err).
+ WithFields(map[string]any{"username": username}).
+ Error("Unable to change password for user: unable to parse request body")
+ ctx.SetJSONError(messageUnableToChangePassword)
+ ctx.SetStatusCode(http.StatusBadRequest)
+
return
}
if err = ctx.Providers.PasswordPolicy.Check(requestBody.NewPassword); err != nil {
- ctx.Error(err, messagePasswordWeak)
+ ctx.Logger.WithError(err).
+ WithFields(map[string]any{"username": username}).
+ Debug("Unable to change password for user as their new password was weak or empty")
+ ctx.SetJSONError(messagePasswordWeak)
+ ctx.SetStatusCode(http.StatusBadRequest)
+
return
}
if err = ctx.Providers.UserProvider.ChangePassword(username, requestBody.OldPassword, requestBody.NewPassword); err != nil {
- ctx.Logger.WithError(err).Debugf("Unable to change password for user '%s'", username)
-
switch {
case errors.Is(err, authentication.ErrIncorrectPassword):
+ ctx.Logger.WithError(err).
+ WithFields(map[string]any{"username": username}).
+ Debug("Unable to change password for user as their old password was incorrect")
ctx.SetJSONError(messageIncorrectPassword)
ctx.SetStatusCode(http.StatusUnauthorized)
case errors.Is(err, authentication.ErrPasswordWeak):
+ ctx.Logger.WithError(err).
+ WithFields(map[string]any{"username": username}).
+ Debug("Unable to change password for user as their new password was weak or empty")
ctx.SetJSONError(messagePasswordWeak)
ctx.SetStatusCode(http.StatusBadRequest)
case errors.Is(err, authentication.ErrAuthenticationFailed):
+ ctx.Logger.WithError(err).
+ WithFields(map[string]any{"username": username}).
+ Error("Unable to change password for user as authentication failed for the user")
ctx.SetJSONError(messageOperationFailed)
ctx.SetStatusCode(http.StatusUnauthorized)
default:
+ ctx.Logger.WithError(err).
+ WithFields(map[string]any{"username": username}).
+ Error("Unable to change password for user for an unknown reason")
ctx.SetJSONError(messageOperationFailed)
ctx.SetStatusCode(http.StatusInternalServerError)
}
@@ -57,10 +90,16 @@ func ChangePasswordPOST(ctx *middlewares.AutheliaCtx) {
return
}
- ctx.Logger.Debugf("User %s has changed their password", username)
+ ctx.Logger.
+ WithFields(map[string]any{"username": username}).
+ Debug("User has changed their password")
+
+ if err = provider.SaveSession(ctx.RequestCtx, userSession); err != nil {
+ ctx.Logger.WithError(err).
+ WithFields(map[string]any{"username": username}).
+ Error("Unable to update password change state")
+ ctx.SetJSONError(messageOperationFailed)
- if err = ctx.SaveSession(userSession); err != nil {
- ctx.Error(fmt.Errorf("unable to update password reset state: %w", err), messageOperationFailed)
return
}
@@ -73,7 +112,8 @@ func ChangePasswordPOST(ctx *middlewares.AutheliaCtx) {
}
if len(userInfo.Emails) == 0 {
- ctx.Logger.Error(fmt.Errorf("user %s has no email address configured", username))
+ ctx.Logger.WithFields(map[string]any{"username": username}).
+ Debug("user has no email address configured")
ctx.ReplyOK()
return
@@ -93,11 +133,19 @@ func ChangePasswordPOST(ctx *middlewares.AutheliaCtx) {
addresses := userInfo.Addresses()
- ctx.Logger.Debugf("Sending an email to user %s (%s) to inform that the password has changed.",
- username, addresses[0].String())
+ ctx.Logger.WithFields(map[string]any{
+ "username": username,
+ "email": addresses[0].String(),
+ }).
+ Debug("Sending an email to inform user that their password has changed.")
if err = ctx.Providers.Notifier.Send(ctx, addresses[0], "Password changed successfully", ctx.Providers.Templates.GetEventEmailTemplate(), data); err != nil {
- ctx.Logger.Error(err)
+ ctx.Logger.WithError(err).
+ WithFields(map[string]any{
+ "username": username,
+ "email": addresses[0].String(),
+ }).
+ Debug("Unable to notify user of password change")
ctx.ReplyOK()
return
diff --git a/internal/handlers/handler_change_password_test.go b/internal/handlers/handler_change_password_test.go
index 3783fef8e..920b2975e 100644
--- a/internal/handlers/handler_change_password_test.go
+++ b/internal/handlers/handler_change_password_test.go
@@ -3,15 +3,15 @@ package handlers
import (
"encoding/json"
"fmt"
+ "regexp"
"testing"
+ "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
"go.uber.org/mock/gomock"
"github.com/authelia/authelia/v4/internal/authentication"
-
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks"
@@ -22,30 +22,13 @@ const (
testPasswordNew = "new_password456"
)
-type ChangePasswordSuite struct {
- suite.Suite
- mock *mocks.MockAutheliaCtx
-}
-
-func (s *ChangePasswordSuite) SetupTest() {
- s.mock = mocks.NewMockAutheliaCtx(s.T())
- userSession, err := s.mock.Ctx.GetSession()
- s.Assert().NoError(err)
-
- userSession.Username = testUsername
- userSession.DisplayName = testUsername
- userSession.Emails[0] = testEmail
- userSession.AuthenticationMethodRefs.UsernameAndPassword = true
- s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
-}
-
-func (s *ChangePasswordSuite) TearDownTest() {
- s.mock.Close()
-}
-
func TestChangePasswordPOST_ShouldSucceedWithValidCredentials(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Ctx.Logger.Logger.SetLevel(logrus.DebugLevel)
+
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
@@ -83,12 +66,18 @@ func TestChangePasswordPOST_ShouldSucceedWithValidCredentials(t *testing.T) {
ChangePasswordPOST(mock.Ctx)
+ mock.AssertLogEntryAdvanced(t, 1, logrus.DebugLevel, "User has changed their password", map[string]any{"username": testUsername})
+
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
}
func TestChangePasswordPOST_ShouldFailWhenPasswordPolicyNotMet(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Ctx.Logger.Logger.SetLevel(logrus.DebugLevel)
+
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
@@ -124,6 +113,8 @@ func TestChangePasswordPOST_ShouldFailWhenPasswordPolicyNotMet(t *testing.T) {
ChangePasswordPOST(mock.Ctx)
+ mock.AssertLogEntryAdvanced(t, 0, logrus.DebugLevel, "Unable to change password for user as their new password was weak or empty", map[string]any{"username": testUsername, "error": "the supplied password does not met the security policy"})
+
errResponse := mock.GetResponseError(t)
assert.Equal(t, "KO", errResponse.Status)
@@ -133,6 +124,10 @@ func TestChangePasswordPOST_ShouldFailWhenPasswordPolicyNotMet(t *testing.T) {
func TestChangePasswordPOST_ShouldFailWhenRequestBodyIsInvalid(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Ctx.Logger.Logger.SetLevel(logrus.DebugLevel)
+
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
@@ -144,6 +139,8 @@ func TestChangePasswordPOST_ShouldFailWhenRequestBodyIsInvalid(t *testing.T) {
ChangePasswordPOST(mock.Ctx)
+ mock.AssertLogEntryAdvanced(t, 0, logrus.ErrorLevel, "Unable to change password for user: unable to parse request body", map[string]any{"username": testUsername, "error": regexp.MustCompile(`^(unable to parse body: .+|unable to validate body: .+|Body is not valid)$`)})
+
errResponse := mock.GetResponseError(t)
assert.Equal(t, "KO", errResponse.Status)
assert.Equal(t, messageUnableToChangePassword, errResponse.Message)
@@ -152,6 +149,10 @@ func TestChangePasswordPOST_ShouldFailWhenRequestBodyIsInvalid(t *testing.T) {
func TestChangePasswordPOST_ShouldFailWhenOldPasswordIsIncorrect(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Ctx.Logger.Logger.SetLevel(logrus.DebugLevel)
+
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
@@ -179,6 +180,11 @@ func TestChangePasswordPOST_ShouldFailWhenOldPasswordIsIncorrect(t *testing.T) {
ChangePasswordPOST(mock.Ctx)
+ mock.AssertLogEntryAdvanced(t, 0, logrus.DebugLevel, "Unable to change password for user as their old password was incorrect", map[string]any{"username": testUsername, "error": "incorrect password"})
+
+ errorField := mock.Hook.LastEntry().Data["error"]
+ assert.ErrorIs(t, authentication.ErrIncorrectPassword, errorField.(error))
+
errResponse := mock.GetResponseError(t)
assert.Equal(t, "KO", errResponse.Status)
assert.Equal(t, messageIncorrectPassword, errResponse.Message)
@@ -187,6 +193,10 @@ func TestChangePasswordPOST_ShouldFailWhenOldPasswordIsIncorrect(t *testing.T) {
func TestChangePasswordPOST_ShouldFailWhenPasswordReuseIsNotAllowed(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Ctx.Logger.Logger.SetLevel(logrus.DebugLevel)
+
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
@@ -214,6 +224,11 @@ func TestChangePasswordPOST_ShouldFailWhenPasswordReuseIsNotAllowed(t *testing.T
ChangePasswordPOST(mock.Ctx)
+ mock.AssertLogEntryAdvanced(t, 0, logrus.DebugLevel, "Unable to change password for user as their new password was weak or empty", map[string]any{"username": testUsername, "error": "your supplied password does not meet the password policy requirements"})
+
+ errorField := mock.Hook.LastEntry().Data["error"]
+ assert.ErrorIs(t, authentication.ErrPasswordWeak, errorField.(error))
+
errResponse := mock.GetResponseError(t)
assert.Equal(t, "KO", errResponse.Status)
assert.Equal(t, messagePasswordWeak, errResponse.Message)
@@ -221,6 +236,9 @@ func TestChangePasswordPOST_ShouldFailWhenPasswordReuseIsNotAllowed(t *testing.T
func TestChangePasswordPOST_ShouldSucceedButLogErrorWhenUserHasNoEmail(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Ctx.Logger.Logger.SetLevel(logrus.DebugLevel)
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
@@ -255,11 +273,16 @@ func TestChangePasswordPOST_ShouldSucceedButLogErrorWhenUserHasNoEmail(t *testin
ChangePasswordPOST(mock.Ctx)
+ mock.AssertLogEntryAdvanced(t, 1, logrus.DebugLevel, "User has changed their password", map[string]any{"username": testUsername})
+
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
}
func TestChangePasswordPOST_ShouldSucceedButLogErrorWhenNotificationFails(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
+ defer mock.Close()
+
+ mock.Ctx.Logger.Logger.SetLevel(logrus.DebugLevel)
userSession, err := mock.Ctx.GetSession()
assert.NoError(t, err)
@@ -294,9 +317,11 @@ func TestChangePasswordPOST_ShouldSucceedButLogErrorWhenNotificationFails(t *tes
mock.NotifierMock.EXPECT().
Send(mock.Ctx, gomock.Any(), "Password changed successfully", gomock.Any(), gomock.Any()).
- Return(fmt.Errorf("failed to send notification"))
+ Return(fmt.Errorf("notifier: smtp: failed to send message: connection refused"))
ChangePasswordPOST(mock.Ctx)
+ mock.AssertLogEntryAdvanced(t, 0, logrus.DebugLevel, "Unable to notify user of password change", map[string]any{"username": testUsername, "email": nil, "error": regexp.MustCompile(`^notifier: smtp: failed to .*: .+$`)})
+
assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
}
diff --git a/internal/mocks/authelia_ctx.go b/internal/mocks/authelia_ctx.go
index b69f03ab2..f1d7202c2 100644
--- a/internal/mocks/authelia_ctx.go
+++ b/internal/mocks/authelia_ctx.go
@@ -268,16 +268,14 @@ func (m *MockAutheliaCtx) SetRequestBody(t *testing.T, body interface{}) {
m.Ctx.Request.SetBody(bodyBytes)
}
-func (m *MockAutheliaCtx) LogEntryN(i int) *logrus.Entry {
+func (m *MockAutheliaCtx) LogEntryN(n int) *logrus.Entry {
entries := m.Hook.AllEntries()
- n := len(entries) - (1 + i)
-
- if n < 0 {
+ if i := len(entries) - (1 + n); i < 0 {
return nil
+ } else {
+ return entries[i]
}
-
- return entries[n]
}
// AssertKO assert an error response from the service.
@@ -376,6 +374,22 @@ func (m *MockAutheliaCtx) AssertLastLogMessage(t *testing.T, message, err string
}
}
+func (m *MockAutheliaCtx) AssertLogEntryAdvanced(t *testing.T, n int, level logrus.Level, message any, fields map[string]any) {
+ entry := m.LogEntryN(n)
+
+ require.NotNil(t, entry)
+
+ assert.Equal(t, level, entry.Level)
+
+ AssertIsStringEqualOrRegexp(t, message, entry.Message)
+
+ for field, expected := range fields {
+ require.Contains(t, entry.Data, field)
+
+ AssertIsStringEqualOrRegexp(t, expected, entry.Data[field])
+ }
+}
+
// GetResponseData retrieves a response from the service.
func (m *MockAutheliaCtx) GetResponseData(t *testing.T, data interface{}) {
okResponse := middlewares.OKResponse{}
@@ -391,3 +405,26 @@ func (m *MockAutheliaCtx) GetResponseError(t *testing.T) (errResponse middleware
return errResponse
}
+
+func AssertIsStringEqualOrRegexp(t *testing.T, expected any, actual any) {
+ switch v := expected.(type) {
+ case string:
+ switch a := actual.(type) {
+ case error:
+ assert.EqualError(t, a, v)
+ default:
+ assert.Equal(t, v, a)
+ }
+ case *regexp.Regexp:
+ switch a := actual.(type) {
+ case error:
+ assert.Regexp(t, v, a.Error())
+ default:
+ assert.Regexp(t, v, a)
+ }
+ case nil:
+ break
+ default:
+ t.Fatal("Expected value must be a string or Regexp")
+ }
+}