diff options
author | Andrey Molotkov <molotkov-and@ydb.tech> | 2024-12-20 14:16:49 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-20 11:16:49 +0000 |
commit | bc46b2ce82722440d3e0b4226c9927b968ca1e7c (patch) | |
tree | 20971a2b115130c87451243285b2c6566b43ed89 | |
parent | 4ca7f5cf75693c2f379b190f621e681917d88952 (diff) | |
download | ydb-bc46b2ce82722440d3e0b4226c9927b968ca1e7c.tar.gz |
Automatically account lockout after some attempts to login with wrong password (#12578)oidc-1.0.0meta-1.0.0
-rw-r--r-- | ydb/core/protos/auth.proto | 6 | ||||
-rw-r--r-- | ydb/core/tx/schemeshard/schemeshard__login.cpp | 139 | ||||
-rw-r--r-- | ydb/core/tx/schemeshard/schemeshard_impl.cpp | 27 | ||||
-rw-r--r-- | ydb/core/tx/schemeshard/schemeshard_impl.h | 14 | ||||
-rw-r--r-- | ydb/core/tx/schemeshard/schemeshard_schema.h | 12 | ||||
-rw-r--r-- | ydb/core/tx/schemeshard/ut_helpers/helpers.cpp | 22 | ||||
-rw-r--r-- | ydb/core/tx/schemeshard/ut_helpers/helpers.h | 15 | ||||
-rw-r--r-- | ydb/core/tx/schemeshard/ut_login/ut_login.cpp | 611 | ||||
-rw-r--r-- | ydb/library/login/login.cpp | 4 | ||||
-rw-r--r-- | ydb/library/login/login.h | 9 | ||||
-rw-r--r-- | ydb/services/ydb/ydb_ldap_login_ut.cpp | 2 | ||||
-rw-r--r-- | ydb/tests/functional/scheme_tests/canondata/tablet_scheme_tests.TestTabletSchemes.test_tablet_schemes_flat_schemeshard_/flat_schemeshard.schema | 20 | ||||
-rw-r--r-- | ydb/tests/library/common/types.py | 2 |
13 files changed, 760 insertions, 123 deletions
diff --git a/ydb/core/protos/auth.proto b/ydb/core/protos/auth.proto index e01eeda6bb..6e713d7f2c 100644 --- a/ydb/core/protos/auth.proto +++ b/ydb/core/protos/auth.proto @@ -57,6 +57,7 @@ message TAuthConfig { optional bool EnableLoginAuthentication = 81 [default = true]; optional string NodeRegistrationToken = 82 [default = "root@builtin", (Ydb.sensitive) = true]; optional TPasswordComplexity PasswordComplexity = 83; + optional TAccountLockout AccountLockout = 84; } message TUserRegistryConfig { @@ -133,3 +134,8 @@ message TPasswordComplexity { optional string SpecialChars = 6; optional bool CanContainUsername = 7; } + +message TAccountLockout { + optional uint32 AttemptThreshold = 1 [default = 4]; + optional string AttemptResetDuration = 2 [default = "1h"]; +} diff --git a/ydb/core/tx/schemeshard/schemeshard__login.cpp b/ydb/core/tx/schemeshard/schemeshard__login.cpp index 6a9408a3bd..faa80e91b9 100644 --- a/ydb/core/tx/schemeshard/schemeshard__login.cpp +++ b/ydb/core/tx/schemeshard/schemeshard__login.cpp @@ -8,13 +8,15 @@ namespace NSchemeShard { using namespace NTabletFlatExecutor; -struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase { +struct TSchemeShard::TTxLogin : TTransactionBase<TSchemeShard> { TEvSchemeShard::TEvLogin::TPtr Request; TPathId SubDomainPathId; bool NeedPublishOnComplete = false; + THolder<TEvSchemeShard::TEvLoginResult> Result = MakeHolder<TEvSchemeShard::TEvLoginResult>(); + size_t CurrentFailedAttemptCount = 0; TTxLogin(TSelf *self, TEvSchemeShard::TEvLogin::TPtr &ev) - : TRwTxBase(self) + : TTransactionBase<TSchemeShard>(self) , Request(std::move(ev)) {} @@ -34,10 +36,11 @@ struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase { }; } - void DoExecute(TTransactionContext& txc, const TActorContext& ctx) override { + bool Execute(TTransactionContext& txc, const TActorContext& ctx) override { LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD, - "TTxLogin DoExecute" + "TTxLogin Execute" << " at schemeshard: " << Self->TabletID()); + NIceDb::TNiceDb db(txc.DB); if (Self->LoginProvider.IsItTimeToRotateKeys()) { LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD, "TTxLogin RotateKeys at schemeshard: " << Self->TabletID()); std::vector<ui64> keysExpired; @@ -50,7 +53,6 @@ struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase { domainPtr->UpdateSecurityState(Self->LoginProvider.GetSecurityState()); domainPtr->IncSecurityStateVersion(); - NIceDb::TNiceDb db(txc.DB); Self->PersistSubDomainSecurityStateVersion(db, SubDomainPathId, *domainPtr); @@ -67,37 +69,130 @@ struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase { NeedPublishOnComplete = true; } + + return LoginAttempt(db, ctx); } - void DoComplete(const TActorContext &ctx) override { + void Complete(const TActorContext &ctx) override { if (NeedPublishOnComplete) { Self->PublishToSchemeBoard(TTxId(), {SubDomainPathId}, ctx); } - THolder<TEvSchemeShard::TEvLoginResult> result = MakeHolder<TEvSchemeShard::TEvLoginResult>(); + LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD, + "TTxLogin Complete" + << ", result: " << Result->Record.ShortDebugString() + << ", at schemeshard: " << Self->TabletID()); + + ctx.Send(Request->Sender, std::move(Result), 0, Request->Cookie); + } + +private: + bool LoginAttempt(NIceDb::TNiceDb& db, const TActorContext& ctx) { const auto& loginRequest = GetLoginRequest(); - if (loginRequest.ExternalAuth || AppData(ctx)->AuthConfig.GetEnableLoginAuthentication()) { - NLogin::TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest); - if (loginResponse.Error) { - result->Record.SetError(loginResponse.Error); - } - if (loginResponse.Token) { - result->Record.SetToken(loginResponse.Token); - result->Record.SetSanitizedToken(loginResponse.SanitizedToken); + if (!loginRequest.ExternalAuth && !AppData(ctx)->AuthConfig.GetEnableLoginAuthentication()) { + Result->Record.SetError("Login authentication is disabled"); + return true; + } + if (loginRequest.ExternalAuth) { + return HandleExternalAuth(loginRequest); + } + return HandleLoginAuth(loginRequest, db, ctx); + } + + bool HandleExternalAuth(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest) { + const NLogin::TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest); + switch (loginResponse.Status) { + case NLogin::TLoginProvider::TLoginUserResponse::EStatus::SUCCESS: { + Result->Record.SetToken(loginResponse.Token); + Result->Record.SetSanitizedToken(loginResponse.SanitizedToken); + break; + } + case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD: + case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_USER: + case NLogin::TLoginProvider::TLoginUserResponse::EStatus::UNAVAILABLE_KEY: + case NLogin::TLoginProvider::TLoginUserResponse::EStatus::UNSPECIFIED: { + Result->Record.SetError(loginResponse.Error); + break; + } + } + return true; + } + + bool HandleLoginAuth(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db, const TActorContext& ctx) { + auto row = db.Table<Schema::LoginSids>().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<Schema::LoginSids::FailedAttemptCount>(); + TInstant lastFailedAttempt = TInstant::FromValue(row.GetValue<Schema::LoginSids::LastFailedAttempt>()); + if (CheckAccountLockout()) { + if (ShouldUnlockAccount(lastFailedAttempt)) { + UnlockAccount(loginRequest, db); + } else { + Result->Record.SetError(TStringBuilder() << "User " << loginRequest.User << " is locked out"); + return true; } + } else if (ShouldResetFailedAttemptCount(lastFailedAttempt)) { + ResetFailedAttemptCount(loginRequest, db); + } + const NLogin::TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest); + switch (loginResponse.Status) { + case NLogin::TLoginProvider::TLoginUserResponse::EStatus::SUCCESS: { + HandleLoginAuthSuccess(loginRequest, loginResponse, db); + Result->Record.SetToken(loginResponse.Token); + Result->Record.SetSanitizedToken(loginResponse.SanitizedToken); + break; + } + case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD: { + HandleLoginAuthInvalidPassword(loginRequest, loginResponse, db); + 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: { + Result->Record.SetError(loginResponse.Error); + break; + } + } + return true; + } + + bool CheckAccountLockout() const { + return (Self->AccountLockout.AttemptThreshold != 0 && CurrentFailedAttemptCount >= Self->AccountLockout.AttemptThreshold); + } - } else { - result->Record.SetError("Login authentication is disabled"); + bool ShouldResetFailedAttemptCount(const TInstant& lastFailedAttempt) { + if (Self->AccountLockout.AttemptResetDuration == TDuration::Zero()) { + return false; } + return lastFailedAttempt + Self->AccountLockout.AttemptResetDuration < TAppData::TimeProvider->Now(); + } - LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD, - "TTxLogin DoComplete" - << ", result: " << result->Record.ShortDebugString() - << ", at schemeshard: " << Self->TabletID()); + bool ShouldUnlockAccount(const TInstant& lastFailedAttempt) { + return ShouldResetFailedAttemptCount(lastFailedAttempt); + } + + void ResetFailedAttemptCount(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db) { + db.Table<Schema::LoginSids>().Key(loginRequest.User).Update<Schema::LoginSids::FailedAttemptCount>(Schema::LoginSids::FailedAttemptCount::Default); + CurrentFailedAttemptCount = Schema::LoginSids::FailedAttemptCount::Default; + } + + void UnlockAccount(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db) { + ResetFailedAttemptCount(loginRequest, db); + } - ctx.Send(Request->Sender, std::move(result), 0, Request->Cookie); + void HandleLoginAuthSuccess(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, const NLogin::TLoginProvider::TLoginUserResponse& loginResponse, NIceDb::TNiceDb& db) { + db.Table<Schema::LoginSids>().Key(loginRequest.User).Update<Schema::LoginSids::LastSuccessfulAttempt, Schema::LoginSids::FailedAttemptCount>(TAppData::TimeProvider->Now().MicroSeconds(), Schema::LoginSids::FailedAttemptCount::Default); } + void HandleLoginAuthInvalidPassword(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, const NLogin::TLoginProvider::TLoginUserResponse& loginResponse, NIceDb::TNiceDb& db) { + db.Table<Schema::LoginSids>().Key(loginRequest.User).Update<Schema::LoginSids::LastFailedAttempt, Schema::LoginSids::FailedAttemptCount>(TAppData::TimeProvider->Now().MicroSeconds(), CurrentFailedAttemptCount + 1); + } }; NTabletFlatExecutor::ITransaction* TSchemeShard::CreateTxLogin(TEvSchemeShard::TEvLogin::TPtr &ev) { diff --git a/ydb/core/tx/schemeshard/schemeshard_impl.cpp b/ydb/core/tx/schemeshard/schemeshard_impl.cpp index 9aca9b5724..da81487be9 100644 --- a/ydb/core/tx/schemeshard/schemeshard_impl.cpp +++ b/ydb/core/tx/schemeshard/schemeshard_impl.cpp @@ -4438,6 +4438,20 @@ 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) @@ -4476,6 +4490,7 @@ TSchemeShard::TSchemeShard(const TActorId &tablet, TTabletStorageInfo *info) .SpecialChars = AppData()->AuthConfig.GetPasswordComplexity().GetSpecialChars(), .CanContainUsername = AppData()->AuthConfig.GetPasswordComplexity().GetCanContainUsername() })) + , AccountLockout(AppData()->AuthConfig.GetAccountLockout()) { TabletCountersPtr.Reset(new TProtobufTabletCounters< ESimpleCounters_descriptor, @@ -7162,6 +7177,7 @@ void TSchemeShard::ApplyConsoleConfigs(const NKikimrConfig::TAppConfig& appConfi if (appConfig.HasAuthConfig()) { ConfigureLoginProvider(appConfig.GetAuthConfig(), ctx); + ConfigureAccountLockout(appConfig.GetAuthConfig(), ctx); } if (IsSchemeShardConfigured()) { @@ -7383,6 +7399,17 @@ void TSchemeShard::ConfigureLoginProvider( << ", CanContainUsername# " << (passwordComplexity.CanContainUsername ? "true" : "false")); } +void TSchemeShard::ConfigureAccountLockout( + const ::NKikimrProto::TAuthConfig& config, + const TActorContext &ctx) +{ + AccountLockout = TAccountLockout(config.GetAccountLockout()); + + LOG_NOTICE_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD, + "AccountLockout configured: AttemptThreshold# " << AccountLockout.AttemptThreshold + << ", AttemptResetDuration# " << AccountLockout.AttemptResetDuration.ToString()); +} + void TSchemeShard::StartStopCompactionQueues() { // note, that we don't need to check current state of compaction queue if (IsServerlessDomain(TPath::Init(RootPathId(), this))) { diff --git a/ydb/core/tx/schemeshard/schemeshard_impl.h b/ydb/core/tx/schemeshard/schemeshard_impl.h index 7dc89abd05..2bcf17ab43 100644 --- a/ydb/core/tx/schemeshard/schemeshard_impl.h +++ b/ydb/core/tx/schemeshard/schemeshard_impl.h @@ -36,6 +36,7 @@ #include <ydb/core/protos/counters_schemeshard.pb.h> #include <ydb/core/protos/filestore_config.pb.h> #include <ydb/core/protos/flat_scheme_op.pb.h> +#include <ydb/core/protos/auth.pb.h> #include <ydb/core/sys_view/common/events.h> #include <ydb/core/statistics/events.h> #include <ydb/core/tablet/pipe_tracker.h> @@ -500,6 +501,10 @@ public: const ::NKikimrProto::TAuthConfig& config, const TActorContext &ctx); + void ConfigureAccountLockout( + const ::NKikimrProto::TAuthConfig& config, + const TActorContext &ctx); + void StartStopCompactionQueues(); void WaitForTableProfiles(ui64 importId, ui32 itemIdx); @@ -1463,6 +1468,15 @@ public: 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/schemeshard_schema.h b/ydb/core/tx/schemeshard/schemeshard_schema.h index c3d154d6be..632616f6fc 100644 --- a/ydb/core/tx/schemeshard/schemeshard_schema.h +++ b/ydb/core/tx/schemeshard/schemeshard_schema.h @@ -1628,9 +1628,19 @@ struct Schema : NIceDb::Schema { struct SidName : Column<1, NScheme::NTypeIds::String> {}; struct SidType : Column<2, NScheme::NTypeIds::Uint64> { using Type = NLoginProto::ESidType::SidType; }; struct SidHash : Column<3, NScheme::NTypeIds::String> {}; + struct LastSuccessfulAttempt : Column<4, NScheme::NTypeIds::Timestamp> {}; + struct LastFailedAttempt : Column<5, NScheme::NTypeIds::Timestamp> {}; + struct FailedAttemptCount : Column<6, NScheme::NTypeIds::Uint32> {using Type = ui32; static constexpr Type Default = 0;}; using TKey = TableKey<SidName>; - using TColumns = TableColumns<SidName, SidType, SidHash>; + using TColumns = TableColumns< + SidName, + SidType, + SidHash, + LastSuccessfulAttempt, + LastFailedAttempt, + FailedAttemptCount + >; }; struct LoginSidMembers : Table<94> { diff --git a/ydb/core/tx/schemeshard/ut_helpers/helpers.cpp b/ydb/core/tx/schemeshard/ut_helpers/helpers.cpp index ded89e9701..3517ef7b07 100644 --- a/ydb/core/tx/schemeshard/ut_helpers/helpers.cpp +++ b/ydb/core/tx/schemeshard/ut_helpers/helpers.cpp @@ -17,6 +17,7 @@ #include <ydb/core/util/pb.h> #include <ydb/public/api/protos/ydb_export.pb.h> #include <ydb/core/protos/schemeshard/operations.pb.h> +#include <ydb/core/protos/auth.pb.h> #include <ydb/public/sdk/cpp/client/ydb_table/table.h> #include <library/cpp/testing/unittest/registar.h> @@ -1976,7 +1977,6 @@ namespace NSchemeShardUT_Private { auto transaction = modifyTx->Record.AddTransaction(); transaction->SetWorkingDir(database); transaction->SetOperationType(NKikimrSchemeOp::EOperationType::ESchemeOpAlterLogin); - auto removeUser = transaction->MutableAlterLogin()->MutableRemoveUser(); removeUser->SetUser(user); @@ -1984,6 +1984,18 @@ namespace NSchemeShardUT_Private { TestModificationResults(runtime, txId, expectedResults); } + void CreateAlterLoginCreateGroup(TTestActorRuntime& runtime, ui64 txId, const TString& database, const TString& group, const TVector<TExpectedResult>& expectedResults) { + auto modifyTx = std::make_unique<TEvSchemeShard::TEvModifySchemeTransaction>(txId, TTestTxConfig::SchemeShard); + auto transaction = modifyTx->Record.AddTransaction(); + transaction->SetWorkingDir(database); + transaction->SetOperationType(NKikimrSchemeOp::EOperationType::ESchemeOpAlterLogin); + auto createGroup = transaction->MutableAlterLogin()->MutableCreateGroup(); + createGroup->SetGroup(group); + + AsyncSend(runtime, TTestTxConfig::SchemeShard, modifyTx.release()); + TestModificationResults(runtime, txId, expectedResults); + } + void AlterLoginAddGroupMembership(TTestActorRuntime& runtime, ui64 txId, const TString& database, const TString& member, const TString& group, const TVector<TExpectedResult>& expectedResults) { auto modifyTx = std::make_unique<TEvSchemeShard::TEvModifySchemeTransaction>(txId, TTestTxConfig::SchemeShard); auto transaction = modifyTx->Record.AddTransaction(); @@ -2010,13 +2022,17 @@ namespace NSchemeShardUT_Private { AsyncSend(runtime, TTestTxConfig::SchemeShard, modifyTx.release()); TestModificationResults(runtime, txId, expectedResults); - } - + } + NKikimrScheme::TEvLoginResult Login(TTestActorRuntime& runtime, const TString& user, const TString& password) { TActorId sender = runtime.AllocateEdgeActor(); auto evLogin = new TEvSchemeShard::TEvLogin(); evLogin->Record.SetUser(user); evLogin->Record.SetPassword(password); + + if (auto ldapDomain = runtime.GetAppData().AuthConfig.GetLdapAuthenticationDomain(); user.EndsWith("@" + ldapDomain)) { + evLogin->Record.SetExternalAuth(ldapDomain); + } ForwardToTablet(runtime, TTestTxConfig::SchemeShard, sender, evLogin); TAutoPtr<IEventHandle> handle; auto event = runtime.GrabEdgeEvent<TEvSchemeShard::TEvLoginResult>(handle); diff --git a/ydb/core/tx/schemeshard/ut_helpers/helpers.h b/ydb/core/tx/schemeshard/ut_helpers/helpers.h index f5d4c79c54..04d2c0f194 100644 --- a/ydb/core/tx/schemeshard/ut_helpers/helpers.h +++ b/ydb/core/tx/schemeshard/ut_helpers/helpers.h @@ -523,7 +523,7 @@ namespace NSchemeShardUT_Private { TTestActorRuntime& runtime, ui64 schemeShard, ui64 tabletId, NKikimrScheme::TEvFindTabletSubDomainPathIdResult::EStatus expected = NKikimrScheme::TEvFindTabletSubDomainPathIdResult::SUCCESS); - void CreateAlterLoginCreateUser(TTestActorRuntime& runtime, ui64 txId, const TString& database, + void CreateAlterLoginCreateUser(TTestActorRuntime& runtime, ui64 txId, const TString& database, const TString& user, const TString& password, const TVector<TExpectedResult>& expectedResults = {{NKikimrScheme::StatusSuccess}}); @@ -531,15 +531,18 @@ namespace NSchemeShardUT_Private { const TString& user, const TVector<TExpectedResult>& expectedResults = {{NKikimrScheme::StatusSuccess}}); - void AlterLoginAddGroupMembership(TTestActorRuntime& runtime, ui64 txId, const TString& database, - const TString& member, const TString& group, + void CreateAlterLoginCreateGroup(TTestActorRuntime& runtime, ui64 txId, const TString& database, + const TString& group, const TVector<TExpectedResult>& expectedResults = {{NKikimrScheme::StatusSuccess}}); + + void AlterLoginAddGroupMembership(TTestActorRuntime& runtime, ui64 txId, const TString& database, + const TString& member, const TString& group, const TVector<TExpectedResult>& expectedResults = {{NKikimrScheme::StatusSuccess}}); - void AlterLoginRemoveGroupMembership(TTestActorRuntime& runtime, ui64 txId, const TString& database, - const TString& member, const TString& group, + void AlterLoginRemoveGroupMembership(TTestActorRuntime& runtime, ui64 txId, const TString& database, + const TString& member, const TString& group, const TVector<TExpectedResult>& expectedResults = {{NKikimrScheme::StatusSuccess}}); - NKikimrScheme::TEvLoginResult Login(TTestActorRuntime& runtime, + NKikimrScheme::TEvLoginResult Login(TTestActorRuntime& runtime, const TString& user, const TString& password); // Mimics data query to a single table with multiple partitions diff --git a/ydb/core/tx/schemeshard/ut_login/ut_login.cpp b/ydb/core/tx/schemeshard/ut_login/ut_login.cpp index b713052e5f..f35f400520 100644 --- a/ydb/core/tx/schemeshard/ut_login/ut_login.cpp +++ b/ydb/core/tx/schemeshard/ut_login/ut_login.cpp @@ -33,6 +33,42 @@ void SetPasswordCheckerParameters(TTestActorRuntime &runtime, ui64 schemeShard, SetConfig(runtime, schemeShard, std::move(request)); } +struct TAccountLockoutInitializer { + size_t AttemptThreshold = 4; + TString AttemptResetDuration = "1h"; +}; + +void SetAccountLockoutParameters(TTestActorRuntime &runtime, ui64 schemeShard, const TAccountLockoutInitializer& initializer) { + auto request = MakeHolder<NConsole::TEvConsole::TEvConfigNotificationRequest>(); + + ::NKikimrProto::TAccountLockout accountLockout; + accountLockout.SetAttemptThreshold(initializer.AttemptThreshold); + accountLockout.SetAttemptResetDuration(initializer.AttemptResetDuration); + *request->Record.MutableConfig()->MutableAuthConfig()->MutableAccountLockout() = accountLockout; + SetConfig(runtime, schemeShard, std::move(request)); +} + +struct TCheckSecurityStateExpectedValues { + size_t PublicKeysSize; + size_t SidsSize; +}; + +void CheckSecurityState(const NKikimrScheme::TEvDescribeSchemeResult& describeResult, const TCheckSecurityStateExpectedValues& expectedValues) { + UNIT_ASSERT(describeResult.HasPathDescription()); + UNIT_ASSERT(describeResult.GetPathDescription().HasDomainDescription()); + UNIT_ASSERT(describeResult.GetPathDescription().GetDomainDescription().HasSecurityState()); + UNIT_ASSERT_VALUES_EQUAL(describeResult.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize(), expectedValues.PublicKeysSize); + UNIT_ASSERT_VALUES_EQUAL(describeResult.GetPathDescription().GetDomainDescription().GetSecurityState().SidsSize(), expectedValues.SidsSize); +} + +void CheckToken(const TString& token, const NKikimrScheme::TEvDescribeSchemeResult& describeResult, const TString& expectedUsername, const TString& expectedError = "") { + NLogin::TLoginProvider login; + login.UpdateSecurityState(describeResult.GetPathDescription().GetDomainDescription().GetSecurityState()); + auto validateResult = login.ValidateToken({.Token = token}); + UNIT_ASSERT_VALUES_EQUAL(validateResult.Error, expectedError); + UNIT_ASSERT_VALUES_EQUAL(validateResult.User, expectedUsername); +} + } // namespace NSchemeShardUT_Private Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { @@ -45,22 +81,14 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { { auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); Cerr << describe.DebugString() << Endl; - UNIT_ASSERT(describe.HasPathDescription()); - UNIT_ASSERT(describe.GetPathDescription().HasDomainDescription()); - UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().HasSecurityState()); - UNIT_ASSERT_VALUES_EQUAL(describe.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize(), 0); - UNIT_ASSERT_VALUES_EQUAL(describe.GetPathDescription().GetDomainDescription().GetSecurityState().SidsSize(), 0); + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); } CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); - + { auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); - UNIT_ASSERT(describe.HasPathDescription()); - UNIT_ASSERT(describe.GetPathDescription().HasDomainDescription()); - UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().HasSecurityState()); - UNIT_ASSERT_VALUES_EQUAL(describe.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize(), 0); - UNIT_ASSERT_VALUES_EQUAL(describe.GetPathDescription().GetDomainDescription().GetSecurityState().SidsSize(), 1); + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 1}); } // public keys are filled after the first login @@ -69,20 +97,13 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { { auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); - UNIT_ASSERT(describe.HasPathDescription()); - UNIT_ASSERT(describe.GetPathDescription().HasDomainDescription()); - UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().HasSecurityState()); - UNIT_ASSERT_VALUES_EQUAL(describe.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize(), 1); - UNIT_ASSERT_VALUES_EQUAL(describe.GetPathDescription().GetDomainDescription().GetSecurityState().SidsSize(), 1); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); } // check token { auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); - NLogin::TLoginProvider login; - login.UpdateSecurityState(describe.GetPathDescription().GetDomainDescription().GetSecurityState()); - auto resultValidate = login.ValidateToken({.Token = resultLogin.token()}); - UNIT_ASSERT_VALUES_EQUAL(resultValidate.User, "user1"); + CheckToken(resultLogin.token(), describe, "user1"); } } @@ -93,7 +114,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); auto resultLogin = Login(runtime, "user1", "password1"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); - + AsyncMkDir(runtime, ++txId, "/MyRoot", "Dir1"); TestModificationResult(runtime, txId, NKikimrScheme::StatusAccepted); AsyncMkDir(runtime, ++txId, "/MyRoot", "Dir2"); @@ -137,7 +158,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { // check login { auto resultLogin = Login(runtime, "user1", "password1"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.GetError(), "Invalid user"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.GetError(), "Cannot find user: user1"); } // another still has access @@ -152,7 +173,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); auto resultLogin = Login(runtime, "user1", "password1"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); - + AsyncMkDir(runtime, ++txId, "/MyRoot", "Dir1"); TestModificationResult(runtime, txId, NKikimrScheme::StatusAccepted); AsyncMkDir(runtime, ++txId, "/MyRoot", "Dir2"); @@ -192,7 +213,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { // check login { auto resultLogin = Login(runtime, "user1", "password1"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.GetError(), "Invalid user"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.GetError(), "Cannot find user: user1"); } // another still has access @@ -204,7 +225,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { TTestBasicRuntime runtime; TTestEnv env(runtime); ui64 txId = 100; - + for (bool missingOk : { false, true}) { auto modifyTx = std::make_unique<TEvSchemeShard::TEvModifySchemeTransaction>(txId, TTestTxConfig::SchemeShard); auto transaction = modifyTx->Record.AddTransaction(); @@ -230,7 +251,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); auto resultLogin = Login(runtime, "user1", "password1"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); - + AsyncMkDir(runtime, ++txId, "/MyRoot", "Dir1/DirSub1"); NACLib::TDiffACL diffACL; @@ -246,7 +267,7 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { CreateAlterLoginRemoveUser(runtime, ++txId, "/MyRoot", "user1", TVector<TExpectedResult>{{NKikimrScheme::StatusPreconditionFailed, "User user1 owns /MyRoot/Dir1/DirSub1 and can't be removed"}}); - + // check user still exists and has their rights: { TestDescribeResult(DescribePath(runtime, "/MyRoot/Dir1/DirSub1"), @@ -264,130 +285,542 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) { TestDescribeResult(DescribePath(runtime, "/MyRoot/Dir1/DirSub1"), {NLs::HasNoRight("+U:user1"), NLs::HasNoEffectiveRight("+U:user1"), NLs::HasOwner("user2")}); auto resultLogin = Login(runtime, "user1", "password1"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.GetError(), "Invalid user"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.GetError(), "Cannot find user: user1"); } } + Y_UNIT_TEST(TestExternalLogin) { + TTestBasicRuntime runtime; + TTestEnv env(runtime); + auto resultLogin = Login(runtime, "user1@ldap", "password1"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 0}); + CheckToken(resultLogin.token(), describe, "user1@ldap"); + } + + Y_UNIT_TEST(TestExternalLoginWithIncorrectLdapDomain) { + TTestBasicRuntime runtime; + TTestEnv env(runtime); + auto resultLogin = Login(runtime, "ldapuser@ldap.domain", "password1"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Cannot find user: ldapuser@ldap.domain"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.token(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 0}); + } + Y_UNIT_TEST(DisableBuiltinAuthMechanism) { TTestBasicRuntime runtime; TTestEnv env(runtime); runtime.GetAppData().AuthConfig.SetEnableLoginAuthentication(false); ui64 txId = 100; - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1", {{NKikimrScheme::StatusPreconditionFailed, "Login authentication is disabled"}}); + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + Cerr << describe.DebugString() << Endl; + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); + } + + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1", {{NKikimrScheme::StatusPreconditionFailed}}); auto resultLogin = Login(runtime, "user1", "password1"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Login authentication is disabled"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.token(), ""); - auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); - UNIT_ASSERT(describe.HasPathDescription()); - UNIT_ASSERT(describe.GetPathDescription().HasDomainDescription()); - UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().HasSecurityState()); - UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize() > 0); + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 0}); + } + } + + Y_UNIT_TEST(FailedLoginWithInvalidUser) { + TTestBasicRuntime runtime; + TTestEnv env(runtime); + ui64 txId = 100; + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + Cerr << describe.DebugString() << Endl; + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); + } + + CreateAlterLoginCreateGroup(runtime, ++txId, "/MyRoot", "group1"); + auto resultLogin = Login(runtime, "group1", "password1"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid user"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.token(), ""); + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + } } Y_UNIT_TEST(ChangeAcceptablePasswordParameters) { TTestBasicRuntime runtime; TTestEnv env(runtime); ui64 txId = 100; + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + Cerr << describe.DebugString() << Endl; + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); + } + // Password parameters: // min length 0 // optional: lower case, upper case, numbers, special symbols from list !@#$%^&*()_+{}|<>?= // required: cannot contain username - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); - auto resultLogin = Login(runtime, "user1", "password1"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); - auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); - UNIT_ASSERT(describe.HasPathDescription()); - UNIT_ASSERT(describe.GetPathDescription().HasDomainDescription()); - UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().HasSecurityState()); - UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize() > 0); + + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); + auto resultLogin = Login(runtime, "user1", "password1"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + CheckToken(resultLogin.token(), describe, "user1"); + } // Accept password without lower case symbols - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user2", "PASSWORDU2"); - resultLogin = Login(runtime, "user2", "PASSWORDU2"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user2", "PASSWORDU2"); + auto resultLogin = Login(runtime, "user2", "PASSWORDU2"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 2}); + CheckToken(resultLogin.token(), describe, "user2"); + } + // Password parameters: // min length 0 // optional: upper case, numbers, special symbols from list !@#$%^&*()_+{}|<>?= // required: lower case = 3, cannot contain username - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLowerCaseCount = 3}); - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user3", "PASSWORDU3", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 lower case character"}}); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLowerCaseCount = 3}); + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user3", "PASSWORDU3", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 lower case character"}}); + auto resultLogin = Login(runtime, "user3", "PASSWORDU3"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Cannot find user: user3"); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 2}); + } + // Add lower case symbols to password - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user3", "PASswORDu3"); - resultLogin = Login(runtime, "user3", "PASswORDu3"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user3", "PASswORDu3"); + auto resultLogin = Login(runtime, "user3", "PASswORDu3"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 3}); + CheckToken(resultLogin.token(), describe, "user3"); + } // Accept password without upper case symbols - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user4", "passwordu4"); - resultLogin = Login(runtime, "user4", "passwordu4"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user4", "passwordu4"); + auto resultLogin = Login(runtime, "user4", "passwordu4"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 4}); + CheckToken(resultLogin.token(), describe, "user4"); + } + // Password parameters: // min length 0 // optional: lower case, numbers, special symbols from list !@#$%^&*()_+{}|<>?= // required: upper case = 3, cannot contain username - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLowerCaseCount = 0, .MinUpperCaseCount = 3}); - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user5", "passwordu5", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 upper case character"}}); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLowerCaseCount = 0, .MinUpperCaseCount = 3}); + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user5", "passwordu5", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 upper case character"}}); + auto resultLogin = Login(runtime, "user5", "passwordu5"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Cannot find user: user5"); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 4}); + } + // Add 3 upper case symbols to password - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user5", "PASswORDu5"); - resultLogin = Login(runtime, "user5", "PASswORDu5"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user5", "PASswORDu5"); + auto resultLogin = Login(runtime, "user5", "PASswORDu5"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 5}); + CheckToken(resultLogin.token(), describe, "user5"); + } // Accept short password - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinUpperCaseCount = 0}); - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user6", "passwu6"); - resultLogin = Login(runtime, "user6", "passwu6"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinUpperCaseCount = 0}); + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user6", "passwu6"); + auto resultLogin = Login(runtime, "user6", "passwu6"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 6}); + CheckToken(resultLogin.token(), describe, "user6"); + } + // Password parameters: // min length 8 // optional: lower case, upper case, numbers, special symbols from list !@#$%^&*()_+{}|<>?= // required: cannot contain username - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLength = 8}); // Too short password - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user7", "passwu7", {{NKikimrScheme::StatusPreconditionFailed, "Password is too short"}}); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLength = 8}); + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user7", "passwu7", {{NKikimrScheme::StatusPreconditionFailed, "Password is too short"}}); + auto resultLogin = Login(runtime, "user7", "passwu7"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Cannot find user: user7"); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 6}); + } + // Password has correct length - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user7", "passwordu7"); - resultLogin = Login(runtime, "user7", "passwordu7"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user7", "passwordu7"); + auto resultLogin = Login(runtime, "user7", "passwordu7"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 7}); + CheckToken(resultLogin.token(), describe, "user7"); + } // Accept password without numbers - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLength = 0}); - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user8", "passWorDueitgh"); - resultLogin = Login(runtime, "user8", "passWorDueitgh"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLength = 0}); + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user8", "passWorDueitgh"); + auto resultLogin = Login(runtime, "user8", "passWorDueitgh"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 8}); + CheckToken(resultLogin.token(), describe, "user8"); + } + // Password parameters: // min length 0 // optional: lower case, upper case,special symbols from list !@#$%^&*()_+{}|<>?= // required: numbers = 3, cannot contain username - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinNumbersCount = 3}); - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user9", "passwordunine", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 number"}}); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinNumbersCount = 3}); + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user9", "passwordunine", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 number"}}); + auto resultLogin = Login(runtime, "user9", "passwordunine"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Cannot find user: user9"); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 8}); + } + // Password with numbers - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user9", "pas1swo5rdu9"); - resultLogin = Login(runtime, "user9", "pas1swo5rdu9"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user9", "pas1swo5rdu9"); + auto resultLogin = Login(runtime, "user9", "pas1swo5rdu9"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 9}); + CheckToken(resultLogin.token(), describe, "user9"); + } // Accept password without special symbols - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinNumbersCount = 0}); - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user10", "passWorDu10"); - resultLogin = Login(runtime, "user10", "passWorDu10"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinNumbersCount = 0}); + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user10", "passWorDu10"); + auto resultLogin = Login(runtime, "user10", "passWorDu10"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 10}); + CheckToken(resultLogin.token(), describe, "user10"); + } + // Password parameters: // min length 0 // optional: lower case, upper case, numbers // required: special symbols from list !@#$%^&*()_+{}|<>?= , cannot contain username - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinSpecialCharsCount = 3}); - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user11", "passwordu11", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 special character"}}); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinSpecialCharsCount = 3}); + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user11", "passwordu11", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 special character"}}); + auto resultLogin = Login(runtime, "user11", "passwordu11"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Cannot find user: user11"); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 10}); + } + // Password with special symbols - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user11", "passwordu11*&%#"); - resultLogin = Login(runtime, "user11", "passwordu11*&%#"); - UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user11", "passwordu11*&%#"); + auto resultLogin = Login(runtime, "user11", "passwordu11*&%#"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 11}); + CheckToken(resultLogin.token(), describe, "user11"); + } + // Password parameters: // min length 0 // optional: lower case, upper case, numbers // required: special symbols from list *# , cannot contain username - SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.SpecialChars = "*#"}); // Only 2 special symbols are valid - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user12", "passwordu12*&%#", {{NKikimrScheme::StatusPreconditionFailed, "Password contains unacceptable characters"}}); - CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user12", "passwordu12*#"); - resultLogin = Login(runtime, "user12", "passwordu12*#"); + { + SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.SpecialChars = "*#"}); // Only 2 special symbols are valid + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user12", "passwordu12*&%#", {{NKikimrScheme::StatusPreconditionFailed, "Password contains unacceptable characters"}}); + auto resultLogin = Login(runtime, "user12", "passwordu12*&%#"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Cannot find user: user12"); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 11}); + } + + { + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user12", "passwordu12*#"); + auto resultLogin = Login(runtime, "user12", "passwordu12*#"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 12}); + CheckToken(resultLogin.token(), describe, "user12"); + } + } + + Y_UNIT_TEST(AccountLockoutAndAutomaticallyUnlock) { + TTestBasicRuntime runtime; + TTestEnv env(runtime); + auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); + ui64 txId = 100; + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); + } + + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); + NKikimrScheme::TEvLoginResult resultLogin; + for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold(); 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"); + 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}); + } + + // User is blocked for 1 hour + runtime.AdvanceCurrentTime(TDuration::Minutes(61)); + + resultLogin = Login(runtime, "user1", "wrongpassword6"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid password"); + resultLogin = Login(runtime, "user1", "password1"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + CheckToken(resultLogin.token(), describe, "user1"); + } + } + + Y_UNIT_TEST(ResetFailedAttemptCount) { + TTestBasicRuntime runtime; + TTestEnv env(runtime); + auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); + ui64 txId = 100; + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); + } + + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); + NKikimrScheme::TEvLoginResult resultLogin; + for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold() - 1; attempt++) { + resultLogin = Login(runtime, "user1", TStringBuilder() << "wrongpassword" << attempt); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid password"); + } + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + } + + // FailedAttemptCount will reset in 1 hour + runtime.AdvanceCurrentTime(TDuration::Minutes(61)); + + // FailedAttemptCount should be reset + for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold() - 1; attempt++) { + resultLogin = Login(runtime, "user1", TStringBuilder() << "wrongpassword" << accountLockoutConfig.GetAttemptThreshold() + attempt); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid password"); + } + resultLogin = Login(runtime, "user1", "password1"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + CheckToken(resultLogin.token(), describe, "user1"); + } + } + + Y_UNIT_TEST(ChangeAccountLockoutParameters) { + TTestBasicRuntime runtime; + TTestEnv env(runtime); + auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); + ui64 txId = 100; + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); + } + + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); + NKikimrScheme::TEvLoginResult resultLogin; + for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold(); 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}); + } + + // user is blocked for 1 hour + runtime.AdvanceCurrentTime(TDuration::Minutes(61)); + + // Unlock user after 1 hour + resultLogin = Login(runtime, "user1", "password1"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + CheckToken(resultLogin.token(), describe, "user1"); + } + + size_t newAttemptThreshold = 6; + SetAccountLockoutParameters(runtime, TTestTxConfig::SchemeShard, {.AttemptThreshold = newAttemptThreshold, .AttemptResetDuration = "5h"}); + + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user2", "password2"); + // Now user2 have 6 attempts to login + for (size_t attempt = 0; attempt < newAttemptThreshold; attempt++) { + resultLogin = Login(runtime, "user2", TStringBuilder() << "wrongpassword2" << attempt); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "Invalid password"); + } + resultLogin = Login(runtime, "user2", TStringBuilder() << "wrongpassword2" << newAttemptThreshold); + // User is locked out after 6 attempts + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), TStringBuilder() << "User user2 is locked out"); + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + 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)); + 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)); + + // Unlock user after 10 sec + resultLogin = Login(runtime, "user2", "password2"); UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), ""); + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 2}); + CheckToken(resultLogin.token(), describe, "user2"); + } + } + + Y_UNIT_TEST(UserStayLockedOutIfEnterValidPassword) { + TTestBasicRuntime runtime; + TTestEnv env(runtime); + auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); + ui64 txId = 100; + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); + } + + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); + NKikimrScheme::TEvLoginResult resultLogin; + for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold(); 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"); + UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), TStringBuilder() << "User user1 is locked out"); + } + + void CheckUserIsLockedOutPermanently(TTestBasicRuntime& runtime) { + TTestEnv env(runtime); + auto accountLockoutConfig = runtime.GetAppData().AuthConfig.GetAccountLockout(); + ui64 txId = 100; + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 0, .SidsSize = 0}); + } + + CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1"); + NKikimrScheme::TEvLoginResult resultLogin; + for (size_t attempt = 0; attempt < accountLockoutConfig.GetAttemptThreshold(); 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"); + 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}); + } + + // User is blocked permanently + runtime.AdvanceCurrentTime(TDuration::Days(365)); + + // After 1 year user is locked out + resultLogin = Login(runtime, "user1", "wrongpassword365"); + 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"); + + { + auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot"); + CheckSecurityState(describe, {.PublicKeysSize = 1, .SidsSize = 1}); + } + } + + 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); } } diff --git a/ydb/library/login/login.cpp b/ydb/library/login/login.cpp index 0a4282d3b1..a038e2c458 100644 --- a/ydb/library/login/login.cpp +++ b/ydb/library/login/login.cpp @@ -320,17 +320,20 @@ TLoginProvider::TLoginUserResponse TLoginProvider::LoginUser(const TLoginUserReq if (!request.ExternalAuth) { auto itUser = Sids.find(request.User); if (itUser == Sids.end() || itUser->second.Type != ESidType::USER) { + response.Status = TLoginUserResponse::EStatus::INVALID_USER; response.Error = "Invalid user"; return response; } if (!Impl->VerifyHash(request.Password, itUser->second.Hash)) { + response.Status = TLoginUserResponse::EStatus::INVALID_PASSWORD; response.Error = "Invalid password"; return response; } } if (Keys.empty() || Keys.back().PrivateKey.empty()) { + response.Status = TLoginUserResponse::EStatus::UNAVAILABLE_KEY; response.Error = "No key to generate token"; return response; } @@ -372,6 +375,7 @@ TLoginProvider::TLoginUserResponse TLoginProvider::LoginUser(const TLoginUserReq response.Token = TString(encoded_token); response.SanitizedToken = SanitizeJwtToken(response.Token); + response.Status = TLoginUserResponse::EStatus::SUCCESS; return response; } diff --git a/ydb/library/login/login.h b/ydb/library/login/login.h index 5ee672d5b5..1a1ffd121c 100644 --- a/ydb/library/login/login.h +++ b/ydb/library/login/login.h @@ -48,8 +48,17 @@ public: }; struct TLoginUserResponse : TBasicResponse { + enum class EStatus { + UNSPECIFIED, + SUCCESS, + INVALID_USER, + INVALID_PASSWORD, + UNAVAILABLE_KEY + }; + TString Token; TString SanitizedToken; // Token for audit logs + EStatus Status = EStatus::UNSPECIFIED; }; struct TValidateTokenRequest : TBasicRequest { diff --git a/ydb/services/ydb/ydb_ldap_login_ut.cpp b/ydb/services/ydb/ydb_ldap_login_ut.cpp index 0000bba5fe..206f24dad1 100644 --- a/ydb/services/ydb/ydb_ldap_login_ut.cpp +++ b/ydb/services/ydb/ydb_ldap_login_ut.cpp @@ -412,7 +412,7 @@ Y_UNIT_TEST_SUITE(TGRpcLdapAuthentication) { auto factory = CreateLoginCredentialsProviderFactory({.User = login + incorrectLdapDomain, .Password = password}); TLoginClientConnection loginConnection(InitLdapSettings); auto loginProvider = factory->CreateProvider(loginConnection.GetCoreFacility()); - UNIT_ASSERT_EXCEPTION_CONTAINS(loginProvider->GetAuthInfo(), yexception, "Invalid user"); + UNIT_ASSERT_EXCEPTION_CONTAINS(loginProvider->GetAuthInfo(), yexception, "Cannot find user: ldapuser@ldap.domain"); loginConnection.Stop(); } diff --git a/ydb/tests/functional/scheme_tests/canondata/tablet_scheme_tests.TestTabletSchemes.test_tablet_schemes_flat_schemeshard_/flat_schemeshard.schema b/ydb/tests/functional/scheme_tests/canondata/tablet_scheme_tests.TestTabletSchemes.test_tablet_schemes_flat_schemeshard_/flat_schemeshard.schema index 109300a19c..cd58d3db75 100644 --- a/ydb/tests/functional/scheme_tests/canondata/tablet_scheme_tests.TestTabletSchemes.test_tablet_schemes_flat_schemeshard_/flat_schemeshard.schema +++ b/ydb/tests/functional/scheme_tests/canondata/tablet_scheme_tests.TestTabletSchemes.test_tablet_schemes_flat_schemeshard_/flat_schemeshard.schema @@ -6594,6 +6594,21 @@ "ColumnId": 3, "ColumnName": "SidHash", "ColumnType": "String" + }, + { + "ColumnId": 4, + "ColumnName": "LastSuccessfulAttempt", + "ColumnType": "Timestamp" + }, + { + "ColumnId": 5, + "ColumnName": "LastFailedAttempt", + "ColumnType": "Timestamp" + }, + { + "ColumnId": 6, + "ColumnName": "FailedAttemptCount", + "ColumnType": "Uint32" } ], "ColumnsDropped": [], @@ -6602,7 +6617,10 @@ "Columns": [ 1, 2, - 3 + 3, + 4, + 5, + 6 ], "RoomID": 0, "Codec": 0, diff --git a/ydb/tests/library/common/types.py b/ydb/tests/library/common/types.py index 05d023c35e..6758c1789b 100644 --- a/ydb/tests/library/common/types.py +++ b/ydb/tests/library/common/types.py @@ -248,6 +248,8 @@ class PType(AbstractTypeEnum): Double = _ptype_from(32, float_in(-100, 100), float, proto_field='Double') Float = _ptype_from(33, float_in(-100, 100), float, proto_field='Float') + Timestamp = _ptype_from(50, int_between(0, 2 ** 64 - 1), int, proto_field='Timestamp', min_value=0, max_value=2 ** 64 - 1) + # Rework Pair later PairUi64Ui64 = _ptype_from(257, int_between(0, 2 ** 64 - 1), int) |