package regulation_test import ( "context" "testing" "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/models" "github.com/authelia/authelia/v4/internal/regulation" ) type RegulatorSuite struct { suite.Suite ctx context.Context ctrl *gomock.Controller storageMock *mocks.MockStorage config schema.RegulationConfiguration clock mocks.TestingClock } func (s *RegulatorSuite) SetupTest() { s.ctrl = gomock.NewController(s.T()) s.storageMock = mocks.NewMockStorage(s.ctrl) s.ctx = context.Background() s.config = schema.RegulationConfiguration{ MaxRetries: 3, BanTime: time.Second * 180, FindTime: time.Second * 30, } s.clock.Set(time.Now()) } func (s *RegulatorSuite) TearDownTest() { s.ctrl.Finish() } func (s *RegulatorSuite) TestShouldNotThrowWhenUserIsLegitimate() { attemptsInDB := []models.AuthenticationAttempt{ { Username: "john", Successful: true, Time: s.clock.Now().Add(-4 * time.Minute), }, } s.storageMock.EXPECT(). LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) _, err := regulator.Regulate(s.ctx, "john") assert.NoError(s.T(), err) } // This test checks the case in which a user failed to authenticate many times but always // with a certain amount of time larger than FindTime. Meaning the user should not be banned. func (s *RegulatorSuite) TestShouldNotThrowWhenFailedAuthenticationNotInFindTime() { attemptsInDB := []models.AuthenticationAttempt{ { Username: "john", Successful: false, Time: s.clock.Now().Add(-1 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-90 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-180 * time.Second), }, } s.storageMock.EXPECT(). LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) _, err := regulator.Regulate(s.ctx, "john") assert.NoError(s.T(), err) } // This test checks the case in which a user failed to authenticate many times only a few // seconds ago (meaning we are checking from now back to now-FindTime). func (s *RegulatorSuite) TestShouldBanUserIfLatestAttemptsAreWithinFinTime() { attemptsInDB := []models.AuthenticationAttempt{ { Username: "john", Successful: false, Time: s.clock.Now().Add(-1 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-4 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-6 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-180 * time.Second), }, } s.storageMock.EXPECT(). LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) _, err := regulator.Regulate(s.ctx, "john") assert.Equal(s.T(), regulation.ErrUserIsBanned, err) } // This test checks the case in which a user failed to authenticate many times only a few // seconds ago (meaning we are checking from now-FindTime+X back to now-2FindTime+X knowing that // we are within now and now-BanTime). It means the user has been banned some time ago and is still // banned right now. func (s *RegulatorSuite) TestShouldCheckUserIsStillBanned() { attemptsInDB := []models.AuthenticationAttempt{ { Username: "john", Successful: false, Time: s.clock.Now().Add(-31 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-34 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-36 * time.Second), }, } s.storageMock.EXPECT(). LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) _, err := regulator.Regulate(s.ctx, "john") assert.Equal(s.T(), regulation.ErrUserIsBanned, err) } func (s *RegulatorSuite) TestShouldCheckUserIsNotYetBanned() { attemptsInDB := []models.AuthenticationAttempt{ { Username: "john", Successful: false, Time: s.clock.Now().Add(-34 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-36 * time.Second), }, } s.storageMock.EXPECT(). LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) _, err := regulator.Regulate(s.ctx, "john") assert.NoError(s.T(), err) } func (s *RegulatorSuite) TestShouldCheckUserWasAboutToBeBanned() { attemptsInDB := []models.AuthenticationAttempt{ { Username: "john", Successful: false, Time: s.clock.Now().Add(-14 * time.Second), }, // more than 30 seconds elapsed between this auth and the preceding one. // In that case we don't need to regulate the user even though the number // of retrieved attempts is 3. { Username: "john", Successful: false, Time: s.clock.Now().Add(-94 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-96 * time.Second), }, } s.storageMock.EXPECT(). LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) _, err := regulator.Regulate(s.ctx, "john") assert.NoError(s.T(), err) } func (s *RegulatorSuite) TestShouldCheckRegulationHasBeenResetOnSuccessfulAttempt() { attemptsInDB := []models.AuthenticationAttempt{ { Username: "john", Successful: false, Time: s.clock.Now().Add(-90 * time.Second), }, { Username: "john", Successful: true, Time: s.clock.Now().Add(-93 * time.Second), }, // The user was almost banned but he did a successful attempt. Therefore, even if the next // failure happens within FindTime, he should not be banned. { Username: "john", Successful: false, Time: s.clock.Now().Add(-94 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-96 * time.Second), }, } s.storageMock.EXPECT(). LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) regulator := regulation.NewRegulator(s.config, s.storageMock, &s.clock) _, err := regulator.Regulate(s.ctx, "john") assert.NoError(s.T(), err) } func TestRunRegulatorSuite(t *testing.T) { s := new(RegulatorSuite) suite.Run(t, s) } // This test checks that the regulator is disabled when configuration is set to 0. func (s *RegulatorSuite) TestShouldHaveRegulatorDisabled() { attemptsInDB := []models.AuthenticationAttempt{ { Username: "john", Successful: false, Time: s.clock.Now().Add(-31 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-34 * time.Second), }, { Username: "john", Successful: false, Time: s.clock.Now().Add(-36 * time.Second), }, } s.storageMock.EXPECT(). LoadAuthenticationLogs(s.ctx, gomock.Eq("john"), gomock.Any(), gomock.Eq(10), gomock.Eq(0)). Return(attemptsInDB, nil) // Check Disabled Functionality. config := schema.RegulationConfiguration{ MaxRetries: 0, FindTime: time.Second * 180, BanTime: time.Second * 180, } regulator := regulation.NewRegulator(config, s.storageMock, &s.clock) _, err := regulator.Regulate(s.ctx, "john") assert.NoError(s.T(), err) // Check Enabled Functionality. config = schema.RegulationConfiguration{ MaxRetries: 1, FindTime: time.Second * 180, BanTime: time.Second * 180, } regulator = regulation.NewRegulator(config, s.storageMock, &s.clock) _, err = regulator.Regulate(s.ctx, "john") assert.Equal(s.T(), regulation.ErrUserIsBanned, err) }