diff --git a/ydb/core/tx/schemeshard/schemeshard__init.cpp b/ydb/core/tx/schemeshard/schemeshard__init.cpp index 63df4a3729e1..9a8a0e2591f2 100644 --- a/ydb/core/tx/schemeshard/schemeshard__init.cpp +++ b/ydb/core/tx/schemeshard/schemeshard__init.cpp @@ -3884,7 +3884,9 @@ struct TSchemeShard::TTxInit : public TTransactionBase { sid.SetType(rowset.GetValue()); sid.SetHash(rowset.GetValue()); sid.SetCreatedAt(rowset.GetValueOrDefault()); - sid.SetLastSuccessfulLogin(rowset.GetValue()); + sid.SetFailedLoginAttemptCount(rowset.GetValueOrDefault()); + sid.SetLastFailedLogin(rowset.GetValueOrDefault()); + sid.SetLastSuccessfulLogin(rowset.GetValueOrDefault()); sidIndex[sid.name()] = securityState.SidsSize() - 1; if (!rowset.Next()) { return false; diff --git a/ydb/core/tx/schemeshard/schemeshard__login.cpp b/ydb/core/tx/schemeshard/schemeshard__login.cpp index 2b17e283411c..f3591c21e784 100644 --- a/ydb/core/tx/schemeshard/schemeshard__login.cpp +++ b/ydb/core/tx/schemeshard/schemeshard__login.cpp @@ -8,7 +8,7 @@ namespace NSchemeShard { using namespace NTabletFlatExecutor; -struct TSchemeShard::TTxLogin : TTransactionBase { +struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase { TEvSchemeShard::TEvLogin::TPtr Request; TPathId SubDomainPathId; bool NeedPublishOnComplete = false; @@ -16,7 +16,7 @@ struct TSchemeShard::TTxLogin : TTransactionBase { size_t CurrentFailedAttemptCount = 0; TTxLogin(TSelf *self, TEvSchemeShard::TEvLogin::TPtr &ev) - : TTransactionBase(self) + : TRwTxBase(self) , Request(std::move(ev)) {} @@ -36,7 +36,7 @@ struct TSchemeShard::TTxLogin : TTransactionBase { }; } - bool Execute(TTransactionContext& txc, const TActorContext& ctx) override { + void DoExecute(TTransactionContext& txc, const TActorContext& ctx) override { LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD, "TTxLogin Execute" << " at schemeshard: " << Self->TabletID()); @@ -70,10 +70,10 @@ struct TSchemeShard::TTxLogin : TTransactionBase { NeedPublishOnComplete = true; } - return LoginAttempt(db, ctx); + LoginAttempt(db, ctx); } - void Complete(const TActorContext &ctx) override { + void DoComplete(const TActorContext &ctx) override { if (NeedPublishOnComplete) { Self->PublishToSchemeBoard(TTxId(), {SubDomainPathId}, ctx); } @@ -103,19 +103,20 @@ struct TSchemeShard::TTxLogin : TTransactionBase { return std::find_if(adminSids.begin(), adminSids.end(), hasSid) != adminSids.end(); } - bool LoginAttempt(NIceDb::TNiceDb& db, const TActorContext& ctx) { + void LoginAttempt(NIceDb::TNiceDb& db, const TActorContext& ctx) { const auto& loginRequest = GetLoginRequest(); if (!loginRequest.ExternalAuth && !AppData(ctx)->AuthConfig.GetEnableLoginAuthentication()) { Result->Record.SetError("Login authentication is disabled"); - return true; + return; } if (loginRequest.ExternalAuth) { - return HandleExternalAuth(loginRequest); + HandleExternalAuth(loginRequest); + } else { + HandleLoginAuth(loginRequest, db); } - return HandleLoginAuth(loginRequest, db, ctx); } - bool HandleExternalAuth(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest) { + void HandleExternalAuth(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest) { const NLogin::TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest); switch (loginResponse.Status) { case NLogin::TLoginProvider::TLoginUserResponse::EStatus::SUCCESS: { @@ -132,84 +133,53 @@ struct TSchemeShard::TTxLogin : TTransactionBase { break; } } - return true; } - bool HandleLoginAuth(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db, const TActorContext& /* ctx */) { - auto row = db.Table().Key(loginRequest.User).Select(); - if (!row.IsReady()) { - return false; - } - if (!row.IsValid()) { - Result->Record.SetError(TStringBuilder() << "Cannot find user: " << loginRequest.User); - return true; - } - CurrentFailedAttemptCount = row.GetValueOrDefault(); - TInstant lastFailedAttempt = TInstant::FromValue(row.GetValue()); - if (CheckAccountLockout()) { - if (ShouldUnlockAccount(lastFailedAttempt)) { - UnlockAccount(loginRequest, db); - } else { - Result->Record.SetError(TStringBuilder() << "User " << loginRequest.User << " is locked out"); - return true; + void HandleLoginAuth(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db) { + using namespace NLogin; + const TLoginProvider::TCheckLockOutResponse checkLockOutResponse = Self->LoginProvider.CheckLockOutUser({.User = loginRequest.User}); + switch (checkLockOutResponse.Status) { + case TLoginProvider::TCheckLockOutResponse::EStatus::SUCCESS: + case TLoginProvider::TCheckLockOutResponse::EStatus::INVALID_USER: { + Result->Record.SetError(checkLockOutResponse.Error); + return; + } + case TLoginProvider::TCheckLockOutResponse::EStatus::RESET: { + const auto& sid = Self->LoginProvider.Sids[loginRequest.User]; + db.Table().Key(loginRequest.User).Update(sid.FailedLoginAttemptCount); + break; + } + case TLoginProvider::TCheckLockOutResponse::EStatus::UNLOCKED: + case TLoginProvider::TCheckLockOutResponse::EStatus::UNSPECIFIED: { + break; } - } else if (ShouldResetFailedAttemptCount(lastFailedAttempt)) { - ResetFailedAttemptCount(loginRequest, db); } - const NLogin::TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest); + + const TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest); switch (loginResponse.Status) { - case NLogin::TLoginProvider::TLoginUserResponse::EStatus::SUCCESS: { - HandleLoginAuthSuccess(loginRequest, loginResponse, db); + case TLoginProvider::TLoginUserResponse::EStatus::SUCCESS: { + const auto& sid = Self->LoginProvider.Sids[loginRequest.User]; + db.Table().Key(loginRequest.User).Update(ToInstant(sid.LastSuccessfulLogin).MilliSeconds(), sid.FailedLoginAttemptCount); Result->Record.SetToken(loginResponse.Token); Result->Record.SetSanitizedToken(loginResponse.SanitizedToken); Result->Record.SetIsAdmin(IsAdmin()); break; } - case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD: { - HandleLoginAuthInvalidPassword(loginRequest, loginResponse, db); + case TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD: { + const auto& sid = Self->LoginProvider.Sids[loginRequest.User]; + db.Table().Key(loginRequest.User).Update(ToInstant(sid.LastFailedLogin).MilliSeconds(), sid.FailedLoginAttemptCount); Result->Record.SetError(loginResponse.Error); break; } - case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_USER: - case NLogin::TLoginProvider::TLoginUserResponse::EStatus::UNAVAILABLE_KEY: - case NLogin::TLoginProvider::TLoginUserResponse::EStatus::UNSPECIFIED: { + case TLoginProvider::TLoginUserResponse::EStatus::INVALID_USER: + case TLoginProvider::TLoginUserResponse::EStatus::UNAVAILABLE_KEY: + case TLoginProvider::TLoginUserResponse::EStatus::UNSPECIFIED: { Result->Record.SetError(loginResponse.Error); break; } } - return true; - } - - bool CheckAccountLockout() const { - return (Self->AccountLockout.AttemptThreshold != 0 && CurrentFailedAttemptCount >= Self->AccountLockout.AttemptThreshold); - } - - bool ShouldResetFailedAttemptCount(const TInstant& lastFailedAttempt) { - if (Self->AccountLockout.AttemptResetDuration == TDuration::Zero()) { - return false; - } - return lastFailedAttempt + Self->AccountLockout.AttemptResetDuration < TAppData::TimeProvider->Now(); - } - - bool ShouldUnlockAccount(const TInstant& lastFailedAttempt) { - return ShouldResetFailedAttemptCount(lastFailedAttempt); - } - - void ResetFailedAttemptCount(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db) { - db.Table().Key(loginRequest.User).Update(Schema::LoginSids::FailedAttemptCount::Default); - CurrentFailedAttemptCount = Schema::LoginSids::FailedAttemptCount::Default; - } - - void UnlockAccount(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db) { - ResetFailedAttemptCount(loginRequest, db); - } - - void HandleLoginAuthSuccess(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, const NLogin::TLoginProvider::TLoginUserResponse& loginResponse, NIceDb::TNiceDb& db) { - db.Table().Key(loginRequest.User).Update(loginResponse.LoginAttemptTime, Schema::LoginSids::FailedAttemptCount::Default); - } - - void HandleLoginAuthInvalidPassword(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, const NLogin::TLoginProvider::TLoginUserResponse& /* loginResponse */, NIceDb::TNiceDb& db) { - db.Table().Key(loginRequest.User).Update(TAppData::TimeProvider->Now().MicroSeconds(), CurrentFailedAttemptCount + 1); } }; diff --git a/ydb/core/tx/schemeshard/schemeshard_impl.cpp b/ydb/core/tx/schemeshard/schemeshard_impl.cpp index 6f4fc8471f13..161d5c01c9a6 100644 --- a/ydb/core/tx/schemeshard/schemeshard_impl.cpp +++ b/ydb/core/tx/schemeshard/schemeshard_impl.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -4438,20 +4439,6 @@ TActorId TSchemeShard::TPipeClientFactory::CreateClient(const TActorContext& ctx return clientId; } -TSchemeShard::TAccountLockout::TAccountLockout(const ::NKikimrProto::TAccountLockout& accountLockout) - : AttemptThreshold(accountLockout.GetAttemptThreshold()) -{ - AttemptResetDuration = TDuration::Zero(); - if (accountLockout.GetAttemptResetDuration().empty()) { - return; - } - if (TDuration::TryParse(accountLockout.GetAttemptResetDuration(), AttemptResetDuration)) { - if (AttemptResetDuration.Seconds() == 0) { - AttemptResetDuration = TDuration::Zero(); - } - } -} - TSchemeShard::TSchemeShard(const TActorId &tablet, TTabletStorageInfo *info) : TActor(&TThis::StateInit) , TTabletExecutedFlat(info, tablet, new NMiniKQL::TMiniKQLFactory) @@ -4489,8 +4476,9 @@ TSchemeShard::TSchemeShard(const TActorId &tablet, TTabletStorageInfo *info) .MinSpecialCharsCount = AppData()->AuthConfig.GetPasswordComplexity().GetMinSpecialCharsCount(), .SpecialChars = AppData()->AuthConfig.GetPasswordComplexity().GetSpecialChars(), .CanContainUsername = AppData()->AuthConfig.GetPasswordComplexity().GetCanContainUsername() - })) - , AccountLockout(AppData()->AuthConfig.GetAccountLockout()) + }), {.AttemptThreshold = AppData()->AuthConfig.GetAccountLockout().GetAttemptThreshold(), + .AttemptResetDuration = AppData()->AuthConfig.GetAccountLockout().GetAttemptResetDuration() + }) { TabletCountersPtr.Reset(new TProtobufTabletCounters< ESimpleCounters_descriptor, @@ -7418,11 +7406,16 @@ void TSchemeShard::ConfigureAccountLockout( const ::NKikimrProto::TAuthConfig& config, const TActorContext &ctx) { - AccountLockout = TAccountLockout(config.GetAccountLockout()); + NLogin::TAccountLockout::TInitializer accountLockoutInitializer { + .AttemptThreshold = config.GetAccountLockout().GetAttemptThreshold(), + .AttemptResetDuration = config.GetAccountLockout().GetAttemptResetDuration() + }; + + LoginProvider.UpdateAccountLockout(accountLockoutInitializer); LOG_NOTICE_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD, - "AccountLockout configured: AttemptThreshold# " << AccountLockout.AttemptThreshold - << ", AttemptResetDuration# " << AccountLockout.AttemptResetDuration.ToString()); + "AccountLockout configured: AttemptThreshold# " << accountLockoutInitializer.AttemptThreshold + << ", AttemptResetDuration# " << accountLockoutInitializer.AttemptResetDuration); } void TSchemeShard::StartStopCompactionQueues() { diff --git a/ydb/core/tx/schemeshard/schemeshard_impl.h b/ydb/core/tx/schemeshard/schemeshard_impl.h index 39e6ec1318d8..fa1b91eecb2e 100644 --- a/ydb/core/tx/schemeshard/schemeshard_impl.h +++ b/ydb/core/tx/schemeshard/schemeshard_impl.h @@ -1470,15 +1470,6 @@ class TSchemeShard NLogin::TLoginProvider LoginProvider; - struct TAccountLockout { - size_t AttemptThreshold = 4; - TDuration AttemptResetDuration = TDuration::Hours(1); - - TAccountLockout(const ::NKikimrProto::TAccountLockout& accountLockout); - }; - - TAccountLockout AccountLockout; - private: void OnDetach(const TActorContext &ctx) override; void OnTabletDead(TEvTablet::TEvTabletDead::TPtr &ev, const TActorContext &ctx) override; diff --git a/ydb/core/tx/schemeshard/ut_login/ut_login.cpp b/ydb/core/tx/schemeshard/ut_login/ut_login.cpp index ee5fd263f0fd..72f228e714b0 100644 --- a/ydb/core/tx/schemeshard/ut_login/ut_login.cpp +++ b/ydb/core/tx/schemeshard/ut_login/ut_login.cpp @@ -395,7 +395,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CreateAlterLoginCreateGroup(runtime, ++txId, "/MyRoot", "group1"); auto resultLogin = Login(runtime, "group1", "password1"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid user"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "group1 is a group"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.token(), ""); { @@ -623,6 +623,12 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { Y_UNIT_TEST(AccountLockoutAndAutomaticallyUnlock) { TTestBasicRuntime runtime; + runtime.AddAppDataInit([] (ui32 nodeIdx, NKikimr::TAppData& appData) { + Y_UNUSED(nodeIdx); + auto accountLockout = appData.AuthConfig.MutableAccountLockout(); + accountLockout->SetAttemptThreshold(4); + accountLockout->SetAttemptResetDuration("3s"); + }); TTestEnv env(runtime); auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); ui64 txId = 100; @@ -650,8 +656,8 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); } - // User is blocked for 1 hour - runtime.AdvanceCurrentTime(TDuration::Minutes(61)); + // User is blocked for 3 seconds + Sleep(TDuration::Seconds(4)); resultLogin = Login(runtime, "user1", "wrongpassword6"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid password"); @@ -667,6 +673,12 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { Y_UNIT_TEST(ResetFailedAttemptCount) { TTestBasicRuntime runtime; + runtime.AddAppDataInit([] (ui32 nodeIdx, NKikimr::TAppData& appData) { + Y_UNUSED(nodeIdx); + auto accountLockout = appData.AuthConfig.MutableAccountLockout(); + accountLockout->SetAttemptThreshold(4); + accountLockout->SetAttemptResetDuration("3s"); + }); TTestEnv env(runtime); auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); ui64 txId = 100; @@ -688,8 +700,8 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); } - // FailedAttemptCount will reset in 1 hour - runtime.AdvanceCurrentTime(TDuration::Minutes(61)); + // FailedAttemptCount will reset in 3 seconds + Sleep(TDuration::Seconds(4)); // FailedAttemptCount should be reset for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold() - 1; attempt++) { @@ -708,6 +720,12 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { Y_UNIT_TEST(ChangeAccountLockoutParameters) { TTestBasicRuntime runtime; + runtime.AddAppDataInit([] (ui32 nodeIdx, NKikimr::TAppData& appData) { + Y_UNUSED(nodeIdx); + auto accountLockout = appData.AuthConfig.MutableAccountLockout(); + accountLockout->SetAttemptThreshold(4); + accountLockout->SetAttemptResetDuration("3s"); + }); TTestEnv env(runtime); auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); ui64 txId = 100; @@ -731,10 +749,10 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); } - // user is blocked for 1 hour - runtime.AdvanceCurrentTime(TDuration::Minutes(61)); + // user is blocked for 3 seconds + Sleep(TDuration::Seconds(4)); - // Unlock user after 1 hour + // Unlock user after 3 seconds resultLogin = Login(runtime, "user1", "password1"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); @@ -745,7 +763,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { } size_t newAttemptThreshold = 6; - SetAccountLockoutParameters(runtime, TTestTxConfig::SchemeShard, {.AttemptThreshold = newAttemptThreshold, .AttemptResetDuration = "5h"}); + SetAccountLockoutParameters(runtime, TTestTxConfig::SchemeShard, {.AttemptThreshold = newAttemptThreshold, .AttemptResetDuration = "7s"}); CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user2", "password2"); // Now user2 have 6 attempts to login @@ -762,16 +780,16 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 2}); } - // user2 is blocked for 10 hour - // After 3 hours user2 must be locked out - runtime.AdvanceCurrentTime(TDuration::Hours(3)); + // user2 is blocked for 7 seconds + // After 4 seconds user2 must be locked out + Sleep(TDuration::Seconds(4)); resultLogin = Login(runtime, "user2", "wrongpassword28"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), TStringBuilder() << "User user2 is locked out"); - // After 5 h 1 m user2 must be unlocked - runtime.AdvanceCurrentTime(TDuration::Minutes(5 * 60 + 1)); + // After 7 seconds user2 must be unlocked + Sleep(TDuration::Seconds(8)); - // Unlock user after 10 sec + // Unlock user after 7 sec resultLogin = Login(runtime, "user2", "password2"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); { @@ -806,7 +824,14 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), TStringBuilder() << "User user1 is locked out"); } - void CheckUserIsLockedOutPermanently(TTestBasicRuntime& runtime) { + Y_UNIT_TEST(CheckThatLockedOutParametersIsRestoredFromLocalDb) { + TTestBasicRuntime runtime; + runtime.AddAppDataInit([] (ui32 nodeIdx, NKikimr::TAppData& appData) { + Y_UNUSED(nodeIdx); + auto accountLockout = appData.AuthConfig.MutableAccountLockout(); + accountLockout->SetAttemptThreshold(4); + accountLockout->SetAttemptResetDuration("3s"); + }); TTestEnv env(runtime); auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); ui64 txId = 100; @@ -817,61 +842,47 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { } CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); + // Make 2 failed login attempts NKikimrScheme::TEvLoginResult resultLogin; - for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold(); attempt++) { + for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold() / 2; attempt++) { resultLogin = Login(runtime, "user1", TStringBuilder() << "wrongpassword" << attempt); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid password"); } - resultLogin = Login(runtime, "user1", TStringBuilder() << "wrongpassword" << accountLockoutConfig.GetAttemptThreshold()); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), TStringBuilder() << "User user1 is locked out"); - // Also do not accept correct password - resultLogin = Login(runtime, "user1", "password1"); + TActorId sender = runtime.AllocateEdgeActor(); + RebootTablet(runtime, TTestTxConfig::SchemeShard, sender); + + // After reboot schemeshard user has only 2 attempts to successful login before lock out + for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold() / 2; attempt++) { + resultLogin = Login(runtime, "user1", TStringBuilder() << "wrongpassword" << attempt); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid password"); + } + resultLogin = Login(runtime, "user1", TStringBuilder() << "wrongpassword" << accountLockoutConfig.GetAttemptThreshold()); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), TStringBuilder() << "User user1 is locked out"); { auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); - CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + CheckSecurityState(describe, {.PublicKeysSize = 2, .SidsSize = 1}); } - // User is blocked permanently - runtime.AdvanceCurrentTime(TDuration::Days(365)); + Sleep(TDuration::Seconds(2)); + RebootTablet(runtime, TTestTxConfig::SchemeShard, sender); - // After 1 year user is locked out - resultLogin = Login(runtime, "user1", "wrongpassword365"); + // After reboot schemeshard user1 must be locked out + resultLogin = Login(runtime, "user1", TStringBuilder() << "wrongpassword" << accountLockoutConfig.GetAttemptThreshold()); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), TStringBuilder() << "User user1 is locked out"); - // Also do not accept correct password - resultLogin = Login(runtime, "user1", "password1"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), TStringBuilder() << "User user1 is locked out"); + // User1 must be unlocked in 1 second after reboot schemeshard + Sleep(TDuration::Seconds(2)); + resultLogin = Login(runtime, "user1", "password1"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); { auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); - CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + CheckSecurityState(describe, {.PublicKeysSize = 3, .SidsSize = 1}); + CheckToken(resultLogin.token(), describe, "user1"); } } - - Y_UNIT_TEST(LockOutUserPermanentlyIfAttemptResetDurationIsZeroSeconds) { - TTestBasicRuntime runtime; - runtime.AddAppDataInit([] (ui32 nodeIdx, NKikimr::TAppData& appData) { - Y_UNUSED(nodeIdx); - auto accountLockout = appData.AuthConfig.MutableAccountLockout(); - accountLockout->SetAttemptThreshold(4); - accountLockout->SetAttemptResetDuration("0s"); - }); - CheckUserIsLockedOutPermanently(runtime); - } - - Y_UNIT_TEST(LockOutUserPermanentlyIfAttemptResetDurationCannotParse) { - TTestBasicRuntime runtime; - runtime.AddAppDataInit([] (ui32 nodeIdx, NKikimr::TAppData& appData) { - Y_UNUSED(nodeIdx); - auto accountLockout = appData.AuthConfig.MutableAccountLockout(); - accountLockout->SetAttemptThreshold(4); - accountLockout->SetAttemptResetDuration("blablabla"); - }); - CheckUserIsLockedOutPermanently(runtime); - } } namespace NSchemeShardUT_Private { diff --git a/ydb/library/login/account_lockout/account_lockout.cpp b/ydb/library/login/account_lockout/account_lockout.cpp new file mode 100644 index 000000000000..7bc11e1ead3a --- /dev/null +++ b/ydb/library/login/account_lockout/account_lockout.cpp @@ -0,0 +1,34 @@ +#include +#include "account_lockout.h" + +namespace NLogin { + +TAccountLockout::TAccountLockout() = default; + +TAccountLockout::TAccountLockout(const TInitializer& initializer) + : AttemptThreshold(initializer.AttemptThreshold) +{ + SetAttemptResetDuration(initializer.AttemptResetDuration); +} + +void TAccountLockout::Update(const TInitializer& initializer) { + AttemptThreshold = initializer.AttemptThreshold; + SetAttemptResetDuration(initializer.AttemptResetDuration); +} + +void TAccountLockout::SetAttemptResetDuration(const TString& newAttemptResetDuration) { + TDuration attemptResetDuration = TDuration::Zero(); + if (newAttemptResetDuration.empty()) { + AttemptResetDuration = std::chrono::system_clock::duration(std::chrono::seconds(attemptResetDuration.Seconds())); + return; + } + if (TDuration::TryParse(newAttemptResetDuration, attemptResetDuration)) { + if (attemptResetDuration.Seconds() == 0) { + attemptResetDuration = TDuration::Zero(); + } + } + + AttemptResetDuration = std::chrono::system_clock::duration(std::chrono::seconds(attemptResetDuration.Seconds())); +} + +} // NLogin diff --git a/ydb/library/login/account_lockout/account_lockout.h b/ydb/library/login/account_lockout/account_lockout.h new file mode 100644 index 000000000000..195deab79125 --- /dev/null +++ b/ydb/library/login/account_lockout/account_lockout.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +namespace NLogin { + +struct TAccountLockout { + struct TInitializer { + size_t AttemptThreshold = 0; + TString AttemptResetDuration; + }; + + size_t AttemptThreshold = 4; + std::chrono::system_clock::duration AttemptResetDuration = std::chrono::hours(1); + + TAccountLockout(); + TAccountLockout(const TInitializer& initializer); + + void Update(const TInitializer& initializer); + +private: + void SetAttemptResetDuration(const TString& newAttemptResetDuration); +}; + +} // NLogin diff --git a/ydb/library/login/account_lockout/account_lockout_ut.cpp b/ydb/library/login/account_lockout/account_lockout_ut.cpp new file mode 100644 index 000000000000..e5a64e6dcf36 --- /dev/null +++ b/ydb/library/login/account_lockout/account_lockout_ut.cpp @@ -0,0 +1,31 @@ +#include +#include +#include "account_lockout.h" + +using namespace NLogin; + +Y_UNIT_TEST_SUITE(AccountLockoutTest) { + Y_UNIT_TEST(InitCorrectValue) { + TAccountLockout accountLockout({.AttemptThreshold = 5, .AttemptResetDuration = "5h"}); + UNIT_ASSERT_VALUES_EQUAL(accountLockout.AttemptThreshold, 5); + UNIT_ASSERT_EQUAL(accountLockout.AttemptResetDuration, std::chrono::seconds(5 * 60 * 60)); + } + + Y_UNIT_TEST(InitZeroValue) { + TAccountLockout accountLockout({.AttemptThreshold = 5, .AttemptResetDuration = "0"}); + UNIT_ASSERT_VALUES_EQUAL(accountLockout.AttemptThreshold, 5); + UNIT_ASSERT_EQUAL(accountLockout.AttemptResetDuration, std::chrono::seconds(0)); + } + + Y_UNIT_TEST(InitEmptyValue) { + TAccountLockout accountLockout({.AttemptThreshold = 5, .AttemptResetDuration = ""}); + UNIT_ASSERT_VALUES_EQUAL(accountLockout.AttemptThreshold, 5); + UNIT_ASSERT_EQUAL(accountLockout.AttemptResetDuration, std::chrono::seconds(0)); + } + + Y_UNIT_TEST(InitWrongValue) { + TAccountLockout accountLockout({.AttemptThreshold = 5, .AttemptResetDuration = "balablabla"}); + UNIT_ASSERT_VALUES_EQUAL(accountLockout.AttemptThreshold, 5); + UNIT_ASSERT_EQUAL(accountLockout.AttemptResetDuration, std::chrono::seconds(0)); + } +} diff --git a/ydb/library/login/account_lockout/ut/ya.make b/ydb/library/login/account_lockout/ut/ya.make new file mode 100644 index 000000000000..b65659ca0468 --- /dev/null +++ b/ydb/library/login/account_lockout/ut/ya.make @@ -0,0 +1,9 @@ +UNITTEST_FOR(ydb/library/login/account_lockout) + +PEERDIR() + +SRCS( + account_lockout_ut.cpp +) + +END() diff --git a/ydb/library/login/account_lockout/ya.make b/ydb/library/login/account_lockout/ya.make new file mode 100644 index 000000000000..7d8fa8c9acb3 --- /dev/null +++ b/ydb/library/login/account_lockout/ya.make @@ -0,0 +1,13 @@ +LIBRARY() + +PEERDIR() + +SRCS( + account_lockout.cpp +) + +END() + +RECURSE_FOR_TESTS( + ut +) diff --git a/ydb/library/login/login.cpp b/ydb/library/login/login.cpp index dfc3f1ad246d..127f7bd40e8b 100644 --- a/ydb/library/login/login.cpp +++ b/ydb/library/login/login.cpp @@ -38,11 +38,17 @@ struct TLoginProvider::TImpl { TLoginProvider::TLoginProvider() : Impl(new TImpl()) , PasswordChecker(TPasswordComplexity()) + , AccountLockout() {} -TLoginProvider::TLoginProvider(const TPasswordComplexity& passwordComplexity) +TLoginProvider::TLoginProvider(const TAccountLockout::TInitializer& accountLockoutInitializer) + : TLoginProvider(TPasswordComplexity(), accountLockoutInitializer) +{} + +TLoginProvider::TLoginProvider(const TPasswordComplexity& passwordComplexity, const TAccountLockout::TInitializer& accountLockoutInitializer) : Impl(new TImpl()) , PasswordChecker(passwordComplexity) + , AccountLockout(accountLockoutInitializer) {} TLoginProvider::~TLoginProvider() @@ -80,6 +86,7 @@ TLoginProvider::TBasicResponse TLoginProvider::CreateUser(const TCreateUserReque user.Name = request.User; user.Hash = Impl->GenerateHash(request.Password); user.CreatedAt = std::chrono::system_clock::now(); + user.LastFailedLogin = std::chrono::system_clock::time_point(); return response; } @@ -317,10 +324,67 @@ std::vector TLoginProvider::GetGroupsMembership(const TString& member) return groups; } +bool TLoginProvider::CheckLockout(const TSidRecord& sid) const { + return (AccountLockout.AttemptThreshold != 0 && sid.FailedLoginAttemptCount >= AccountLockout.AttemptThreshold); +} + +void TLoginProvider::ResetFailedLoginAttemptCount(TSidRecord* sid) { + sid->FailedLoginAttemptCount = 0; +} + +void TLoginProvider::UnlockAccount(TSidRecord* sid) { + ResetFailedLoginAttemptCount(sid); +} + +bool TLoginProvider::ShouldResetFailedAttemptCount(const TSidRecord& sid) const { + if (sid.FailedLoginAttemptCount == 0) { + return false; + } + if (AccountLockout.AttemptResetDuration == std::chrono::system_clock::duration::zero()) { + return false; + } + return sid.LastFailedLogin + AccountLockout.AttemptResetDuration < std::chrono::system_clock::now(); +} + +bool TLoginProvider::ShouldUnlockAccount(const TSidRecord& sid) const { + return ShouldResetFailedAttemptCount(sid); +} + +TLoginProvider::TCheckLockOutResponse TLoginProvider::CheckLockOutUser(const TCheckLockOutRequest& request) { + TCheckLockOutResponse response; + auto itUser = Sids.find(request.User); + if (itUser == Sids.end()) { + response.Status = TCheckLockOutResponse::EStatus::INVALID_USER; + response.Error = TStringBuilder() << "Cannot find user: " << request.User; + return response; + } else if (itUser->second.Type != ESidType::USER) { + response.Status = TCheckLockOutResponse::EStatus::INVALID_USER; + response.Error = TStringBuilder() << request.User << " is a group"; + return response; + } + + TSidRecord& sid = itUser->second; + if (CheckLockout(sid)) { + if (ShouldUnlockAccount(sid)) { + UnlockAccount(&sid); + response.Status = TCheckLockOutResponse::EStatus::RESET; + } else { + response.Status = TCheckLockOutResponse::EStatus::SUCCESS; + response.Error = TStringBuilder() << "User " << request.User << " is locked out"; + } + return response; + } else if (ShouldResetFailedAttemptCount(sid)) { + ResetFailedLoginAttemptCount(&sid); + response.Status = TCheckLockOutResponse::EStatus::RESET; + return response; + } + response.Status = TCheckLockOutResponse::EStatus::UNLOCKED; + return response; +} + TLoginProvider::TLoginUserResponse TLoginProvider::LoginUser(const TLoginUserRequest& request) { auto now = std::chrono::system_clock::now(); TLoginUserResponse response; - response.LoginAttemptTime = std::chrono::time_point_cast(now).time_since_epoch().count(); if (Keys.empty() || Keys.back().PrivateKey.empty()) { response.Status = TLoginUserResponse::EStatus::UNAVAILABLE_KEY; @@ -328,6 +392,7 @@ TLoginProvider::TLoginUserResponse TLoginProvider::LoginUser(const TLoginUserReq return response; } + TSidRecord* sid = nullptr; if (!request.ExternalAuth) { auto itUser = Sids.find(request.User); if (itUser == Sids.end() || itUser->second.Type != ESidType::USER) { @@ -336,13 +401,14 @@ TLoginProvider::TLoginUserResponse TLoginProvider::LoginUser(const TLoginUserReq return response; } + sid = &(itUser->second); if (!Impl->VerifyHash(request.Password, itUser->second.Hash)) { response.Status = TLoginUserResponse::EStatus::INVALID_PASSWORD; response.Error = "Invalid password"; + sid->LastFailedLogin = now; + sid->FailedLoginAttemptCount++; return response; } - - itUser->second.LastSuccessfulLogin = response.LoginAttemptTime; } const TKeyRecord& key = Keys.back(); @@ -382,6 +448,11 @@ TLoginProvider::TLoginUserResponse TLoginProvider::LoginUser(const TLoginUserReq response.SanitizedToken = SanitizeJwtToken(response.Token); response.Status = TLoginUserResponse::EStatus::SUCCESS; + if (sid) { + sid->LastSuccessfulLogin = now; + sid->FailedLoginAttemptCount = 0; + } + return response; } @@ -671,12 +742,14 @@ void TLoginProvider::UpdateSecurityState(const NLoginProto::TSecurityState& stat sid.Type = pbSid.GetType(); sid.Name = pbSid.GetName(); sid.Hash = pbSid.GetHash(); - sid.LastSuccessfulLogin = pbSid.GetLastSuccessfulLogin(); for (const auto& pbSubSid : pbSid.GetMembers()) { sid.Members.emplace(pbSubSid); ChildToParentIndex[pbSubSid].emplace(sid.Name); } sid.CreatedAt = std::chrono::system_clock::time_point(std::chrono::milliseconds(pbSid.GetCreatedAt())); + sid.FailedLoginAttemptCount = pbSid.GetFailedLoginAttemptCount(); + sid.LastFailedLogin = std::chrono::system_clock::time_point(std::chrono::milliseconds(pbSid.GetLastFailedLogin())); + sid.LastSuccessfulLogin = std::chrono::system_clock::time_point(std::chrono::milliseconds(pbSid.GetLastSuccessfulLogin())); } } } @@ -693,4 +766,8 @@ void TLoginProvider::UpdatePasswordCheckParameters(const TPasswordComplexity& pa PasswordChecker.Update(passwordComplexity); } +void TLoginProvider::UpdateAccountLockout(const TAccountLockout::TInitializer& accountLockoutInitializer) { + AccountLockout.Update(accountLockoutInitializer); +} + } diff --git a/ydb/library/login/login.h b/ydb/library/login/login.h index bbf0bf0a2cad..f3a644f3a4ad 100644 --- a/ydb/library/login/login.h +++ b/ydb/library/login/login.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace NLogin { @@ -35,6 +36,22 @@ class TLoginProvider { TString Notice; }; + struct TCheckLockOutRequest : TBasicRequest { + TString User; + }; + + struct TCheckLockOutResponse : TBasicResponse { + enum class EStatus { + UNSPECIFIED, + SUCCESS, + UNLOCKED, + INVALID_USER, + RESET, + }; + + EStatus Status = EStatus::UNSPECIFIED; + }; + struct TLoginUserRequest : TBasicRequest { struct TOptions { bool WithUserGroups = false; @@ -53,13 +70,12 @@ class TLoginProvider { SUCCESS, INVALID_USER, INVALID_PASSWORD, - UNAVAILABLE_KEY + UNAVAILABLE_KEY, }; TString Token; TString SanitizedToken; // Token for audit logs EStatus Status = EStatus::UNSPECIFIED; - ui64 LoginAttemptTime; // microseconds }; struct TValidateTokenRequest : TBasicRequest { @@ -146,8 +162,12 @@ class TLoginProvider { TString Name; TString Hash; std::unordered_set Members; - std::chrono::system_clock::time_point CreatedAt; // CreatedAt does not need in describe result. We will not add to security state - ui64 LastSuccessfulLogin; + // CreatedAt, FailedLoginAttemptCount, LastFailedLogin, LastSuccessfulLogin do not need in describe result. + // We will not add these parameters to security state + std::chrono::system_clock::time_point CreatedAt; + size_t FailedLoginAttemptCount = 0; + std::chrono::system_clock::time_point LastFailedLogin; + std::chrono::system_clock::time_point LastSuccessfulLogin; }; // our current audience (database name) @@ -167,6 +187,7 @@ class TLoginProvider { NLoginProto::TSecurityState GetSecurityState() const; void UpdateSecurityState(const NLoginProto::TSecurityState& state); + TCheckLockOutResponse CheckLockOutUser(const TCheckLockOutRequest& request); TLoginUserResponse LoginUser(const TLoginUserRequest& request); TValidateTokenResponse ValidateToken(const TValidateTokenRequest& request); @@ -182,9 +203,11 @@ class TLoginProvider { TRemoveGroupResponse RemoveGroup(const TRemoveGroupRequest& request); void UpdatePasswordCheckParameters(const TPasswordComplexity& passwordComplexity); + void UpdateAccountLockout(const TAccountLockout::TInitializer& accountLockoutInitializer); TLoginProvider(); - TLoginProvider(const TPasswordComplexity& passwordComplexity); + TLoginProvider(const TAccountLockout::TInitializer& accountLockoutInitializer); + TLoginProvider(const TPasswordComplexity& passwordComplexity, const TAccountLockout::TInitializer& accountLockoutInitializer); ~TLoginProvider(); std::vector GetGroupsMembership(const TString& member); @@ -197,10 +220,17 @@ class TLoginProvider { bool CheckSubjectExists(const TString& name, const ESidType::SidType& type); static bool CheckAllowedName(const TString& name); + bool CheckLockout(const TSidRecord& sid) const; + static void ResetFailedLoginAttemptCount(TSidRecord* sid); + static void UnlockAccount(TSidRecord* sid); + bool ShouldResetFailedAttemptCount(const TSidRecord& sid) const; + bool ShouldUnlockAccount(const TSidRecord& sid) const; + struct TImpl; THolder Impl; TPasswordChecker PasswordChecker; + TAccountLockout AccountLockout; }; } diff --git a/ydb/library/login/login_ut.cpp b/ydb/library/login/login_ut.cpp index 5f8bc3b680cc..91c0015ba235 100644 --- a/ydb/library/login/login_ut.cpp +++ b/ydb/library/login/login_ut.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "login.h" using namespace NLogin; @@ -393,4 +394,133 @@ Y_UNIT_TEST_SUITE(Login) { UNIT_ASSERT(sid1.CreatedAt < sid2.CreatedAt); } } + + Y_UNIT_TEST(CannotCheckLockoutNonExistentUser) { + TLoginProvider provider; + provider.Audience = "test_audience1"; + provider.RotateKeys(); + + { + auto checkLockoutResponse = provider.CheckLockOutUser({.User = "nonExistentUser"}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::INVALID_USER); + UNIT_ASSERT_VALUES_EQUAL(checkLockoutResponse.Error, "Cannot find user: nonExistentUser"); + } + + { + auto createGroupResponse = provider.CreateGroup({.Group = "group1"}); + UNIT_ASSERT(!createGroupResponse.Error); + + auto checkLockoutResponse = provider.CheckLockOutUser({.User = "group1"}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::INVALID_USER); + UNIT_ASSERT_VALUES_EQUAL(checkLockoutResponse.Error, "group1 is a group"); + } + } + + Y_UNIT_TEST(AccountLockoutAndAutomaticallyUnlock) { + TAccountLockout::TInitializer accountLockoutInitializer {.AttemptThreshold = 4, .AttemptResetDuration = "3s"}; + TLoginProvider provider(accountLockoutInitializer); + provider.Audience = "test_audience1"; + provider.RotateKeys(); + + TLoginProvider::TCreateUserRequest createUserRequest { + .User = "user1", + .Password = "password1" + }; + auto createUserResponse = provider.CreateUser(createUserRequest); + UNIT_ASSERT(!createUserResponse.Error); + + { + for (size_t attempt = 0; attempt < accountLockoutInitializer.AttemptThreshold; attempt++) { + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::UNLOCKED); + auto loginUserResponse = provider.LoginUser({.User = createUserRequest.User, .Password = TStringBuilder() << "wrongpassword" << attempt}); + UNIT_ASSERT_EQUAL(loginUserResponse.Status, TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD); + UNIT_ASSERT_VALUES_EQUAL(loginUserResponse.Error, "Invalid password"); + } + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::SUCCESS); + } + + Sleep(TDuration::Seconds(4)); + + { + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::RESET); + auto loginUserResponse = provider.LoginUser({.User = createUserRequest.User, .Password = TStringBuilder() << "wrongpassword" << accountLockoutInitializer.AttemptThreshold}); + UNIT_ASSERT_EQUAL(loginUserResponse.Status, TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD); + UNIT_ASSERT_VALUES_EQUAL(loginUserResponse.Error, "Invalid password"); + } + + { + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::UNLOCKED); + auto loginUserResponse = provider.LoginUser({.User = createUserRequest.User, .Password = createUserRequest.Password}); + UNIT_ASSERT_EQUAL(loginUserResponse.Status, TLoginProvider::TLoginUserResponse::EStatus::SUCCESS); + UNIT_ASSERT_VALUES_EQUAL(loginUserResponse.Error, ""); + + auto validateTokenResponse = provider.ValidateToken({.Token = loginUserResponse.Token}); + UNIT_ASSERT_VALUES_EQUAL(validateTokenResponse.Error, ""); + UNIT_ASSERT(validateTokenResponse.User == createUserRequest.User); + } + } + + Y_UNIT_TEST(ResetFailedAttemptCount) { + TAccountLockout::TInitializer accountLockoutInitializer {.AttemptThreshold = 4, .AttemptResetDuration = "3s"}; + TLoginProvider provider(accountLockoutInitializer); + provider.Audience = "test_audience1"; + provider.RotateKeys(); + + TLoginProvider::TCreateUserRequest createUserRequest { + .User = "user1", + .Password = "password1" + }; + auto createUserResponse = provider.CreateUser(createUserRequest); + UNIT_ASSERT(!createUserResponse.Error); + + { + for (size_t attempt = 0; attempt < accountLockoutInitializer.AttemptThreshold - 1; attempt++) { + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::UNLOCKED); + auto loginUserResponse = provider.LoginUser({.User = createUserRequest.User, .Password = TStringBuilder() << "wrongpassword" << attempt}); + UNIT_ASSERT_EQUAL(loginUserResponse.Status, TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD); + UNIT_ASSERT_VALUES_EQUAL(loginUserResponse.Error, "Invalid password"); + } + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::UNLOCKED); + } + + Sleep(TDuration::Seconds(4)); + + { + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::RESET); + auto loginUserResponse = provider.LoginUser({.User = createUserRequest.User, .Password = "wrongpassword1"}); + UNIT_ASSERT_EQUAL(loginUserResponse.Status, TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD); + UNIT_ASSERT_VALUES_EQUAL(loginUserResponse.Error, "Invalid password"); + } + + { + for (size_t attempt = 0; attempt < accountLockoutInitializer.AttemptThreshold - 2; attempt++) { + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::UNLOCKED); + auto loginUserResponse = provider.LoginUser({.User = createUserRequest.User, .Password = TStringBuilder() << "wrongpassword1" << attempt}); + UNIT_ASSERT_EQUAL(loginUserResponse.Status, TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD); + UNIT_ASSERT_VALUES_EQUAL(loginUserResponse.Error, "Invalid password"); + } + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::UNLOCKED); + } + + { + auto checkLockoutResponse = provider.CheckLockOutUser({.User = createUserRequest.User}); + UNIT_ASSERT_EQUAL(checkLockoutResponse.Status, TLoginProvider::TCheckLockOutResponse::EStatus::UNLOCKED); + auto loginUserResponse = provider.LoginUser({.User = createUserRequest.User, .Password = createUserRequest.Password}); + UNIT_ASSERT_EQUAL(loginUserResponse.Status, TLoginProvider::TLoginUserResponse::EStatus::SUCCESS); + UNIT_ASSERT_VALUES_EQUAL(loginUserResponse.Error, ""); + + auto validateTokenResponse = provider.ValidateToken({.Token = loginUserResponse.Token}); + UNIT_ASSERT_VALUES_EQUAL(validateTokenResponse.Error, ""); + UNIT_ASSERT(validateTokenResponse.User == createUserRequest.User); + } + } } diff --git a/ydb/library/login/protos/login.proto b/ydb/library/login/protos/login.proto index 507de1dd441b..c31a8ffd22b6 100644 --- a/ydb/library/login/protos/login.proto +++ b/ydb/library/login/protos/login.proto @@ -22,8 +22,10 @@ message TSid { ESidType.SidType Type = 2; string Hash = 3; repeated string Members = 4; - uint64 CreatedAt = 5; + uint64 CreatedAt = 5; // microseconds uint64 LastSuccessfulLogin = 6; // microseconds + uint64 LastFailedLogin = 7; // microseconds + uint64 FailedLoginAttemptCount = 8; } message TSecurityState { diff --git a/ydb/library/login/ya.make b/ydb/library/login/ya.make index e420beb2db9e..d701a510dd4f 100644 --- a/ydb/library/login/ya.make +++ b/ydb/library/login/ya.make @@ -8,6 +8,7 @@ PEERDIR( library/cpp/string_utils/base64 ydb/library/login/protos ydb/library/login/password_checker + ydb/library/login/account_lockout ) SRCS( @@ -23,4 +24,5 @@ RECURSE_FOR_TESTS( RECURSE( password_checker + account_lockout )