Skip to content

Commit bc46b2c

Browse files
authored
Automatically account lockout after some attempts to login with wrong password (#12578)
1 parent 4ca7f5c commit bc46b2c

File tree

13 files changed

+760
-123
lines changed

13 files changed

+760
-123
lines changed

ydb/core/protos/auth.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ message TAuthConfig {
5757
optional bool EnableLoginAuthentication = 81 [default = true];
5858
optional string NodeRegistrationToken = 82 [default = "root@builtin", (Ydb.sensitive) = true];
5959
optional TPasswordComplexity PasswordComplexity = 83;
60+
optional TAccountLockout AccountLockout = 84;
6061
}
6162

6263
message TUserRegistryConfig {
@@ -133,3 +134,8 @@ message TPasswordComplexity {
133134
optional string SpecialChars = 6;
134135
optional bool CanContainUsername = 7;
135136
}
137+
138+
message TAccountLockout {
139+
optional uint32 AttemptThreshold = 1 [default = 4];
140+
optional string AttemptResetDuration = 2 [default = "1h"];
141+
}

ydb/core/tx/schemeshard/schemeshard__login.cpp

Lines changed: 117 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ namespace NSchemeShard {
88

99
using namespace NTabletFlatExecutor;
1010

11-
struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase {
11+
struct TSchemeShard::TTxLogin : TTransactionBase<TSchemeShard> {
1212
TEvSchemeShard::TEvLogin::TPtr Request;
1313
TPathId SubDomainPathId;
1414
bool NeedPublishOnComplete = false;
15+
THolder<TEvSchemeShard::TEvLoginResult> Result = MakeHolder<TEvSchemeShard::TEvLoginResult>();
16+
size_t CurrentFailedAttemptCount = 0;
1517

1618
TTxLogin(TSelf *self, TEvSchemeShard::TEvLogin::TPtr &ev)
17-
: TRwTxBase(self)
19+
: TTransactionBase<TSchemeShard>(self)
1820
, Request(std::move(ev))
1921
{}
2022

@@ -34,10 +36,11 @@ struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase {
3436
};
3537
}
3638

37-
void DoExecute(TTransactionContext& txc, const TActorContext& ctx) override {
39+
bool Execute(TTransactionContext& txc, const TActorContext& ctx) override {
3840
LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD,
39-
"TTxLogin DoExecute"
41+
"TTxLogin Execute"
4042
<< " at schemeshard: " << Self->TabletID());
43+
NIceDb::TNiceDb db(txc.DB);
4144
if (Self->LoginProvider.IsItTimeToRotateKeys()) {
4245
LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD, "TTxLogin RotateKeys at schemeshard: " << Self->TabletID());
4346
std::vector<ui64> keysExpired;
@@ -50,7 +53,6 @@ struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase {
5053
domainPtr->UpdateSecurityState(Self->LoginProvider.GetSecurityState());
5154
domainPtr->IncSecurityStateVersion();
5255

53-
NIceDb::TNiceDb db(txc.DB);
5456

5557
Self->PersistSubDomainSecurityStateVersion(db, SubDomainPathId, *domainPtr);
5658

@@ -67,37 +69,130 @@ struct TSchemeShard::TTxLogin : TSchemeShard::TRwTxBase {
6769

6870
NeedPublishOnComplete = true;
6971
}
72+
73+
return LoginAttempt(db, ctx);
7074
}
7175

72-
void DoComplete(const TActorContext &ctx) override {
76+
void Complete(const TActorContext &ctx) override {
7377
if (NeedPublishOnComplete) {
7478
Self->PublishToSchemeBoard(TTxId(), {SubDomainPathId}, ctx);
7579
}
7680

77-
THolder<TEvSchemeShard::TEvLoginResult> result = MakeHolder<TEvSchemeShard::TEvLoginResult>();
81+
LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD,
82+
"TTxLogin Complete"
83+
<< ", result: " << Result->Record.ShortDebugString()
84+
<< ", at schemeshard: " << Self->TabletID());
85+
86+
ctx.Send(Request->Sender, std::move(Result), 0, Request->Cookie);
87+
}
88+
89+
private:
90+
bool LoginAttempt(NIceDb::TNiceDb& db, const TActorContext& ctx) {
7891
const auto& loginRequest = GetLoginRequest();
79-
if (loginRequest.ExternalAuth || AppData(ctx)->AuthConfig.GetEnableLoginAuthentication()) {
80-
NLogin::TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest);
81-
if (loginResponse.Error) {
82-
result->Record.SetError(loginResponse.Error);
83-
}
84-
if (loginResponse.Token) {
85-
result->Record.SetToken(loginResponse.Token);
86-
result->Record.SetSanitizedToken(loginResponse.SanitizedToken);
92+
if (!loginRequest.ExternalAuth && !AppData(ctx)->AuthConfig.GetEnableLoginAuthentication()) {
93+
Result->Record.SetError("Login authentication is disabled");
94+
return true;
95+
}
96+
if (loginRequest.ExternalAuth) {
97+
return HandleExternalAuth(loginRequest);
98+
}
99+
return HandleLoginAuth(loginRequest, db, ctx);
100+
}
101+
102+
bool HandleExternalAuth(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest) {
103+
const NLogin::TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest);
104+
switch (loginResponse.Status) {
105+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::SUCCESS: {
106+
Result->Record.SetToken(loginResponse.Token);
107+
Result->Record.SetSanitizedToken(loginResponse.SanitizedToken);
108+
break;
109+
}
110+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD:
111+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_USER:
112+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::UNAVAILABLE_KEY:
113+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::UNSPECIFIED: {
114+
Result->Record.SetError(loginResponse.Error);
115+
break;
116+
}
117+
}
118+
return true;
119+
}
120+
121+
bool HandleLoginAuth(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db, const TActorContext& ctx) {
122+
auto row = db.Table<Schema::LoginSids>().Key(loginRequest.User).Select();
123+
if (!row.IsReady()) {
124+
return false;
125+
}
126+
if (!row.IsValid()) {
127+
Result->Record.SetError(TStringBuilder() << "Cannot find user: " << loginRequest.User);
128+
return true;
129+
}
130+
CurrentFailedAttemptCount = row.GetValueOrDefault<Schema::LoginSids::FailedAttemptCount>();
131+
TInstant lastFailedAttempt = TInstant::FromValue(row.GetValue<Schema::LoginSids::LastFailedAttempt>());
132+
if (CheckAccountLockout()) {
133+
if (ShouldUnlockAccount(lastFailedAttempt)) {
134+
UnlockAccount(loginRequest, db);
135+
} else {
136+
Result->Record.SetError(TStringBuilder() << "User " << loginRequest.User << " is locked out");
137+
return true;
87138
}
139+
} else if (ShouldResetFailedAttemptCount(lastFailedAttempt)) {
140+
ResetFailedAttemptCount(loginRequest, db);
141+
}
142+
const NLogin::TLoginProvider::TLoginUserResponse loginResponse = Self->LoginProvider.LoginUser(loginRequest);
143+
switch (loginResponse.Status) {
144+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::SUCCESS: {
145+
HandleLoginAuthSuccess(loginRequest, loginResponse, db);
146+
Result->Record.SetToken(loginResponse.Token);
147+
Result->Record.SetSanitizedToken(loginResponse.SanitizedToken);
148+
break;
149+
}
150+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_PASSWORD: {
151+
HandleLoginAuthInvalidPassword(loginRequest, loginResponse, db);
152+
Result->Record.SetError(loginResponse.Error);
153+
break;
154+
}
155+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::INVALID_USER:
156+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::UNAVAILABLE_KEY:
157+
case NLogin::TLoginProvider::TLoginUserResponse::EStatus::UNSPECIFIED: {
158+
Result->Record.SetError(loginResponse.Error);
159+
break;
160+
}
161+
}
162+
return true;
163+
}
164+
165+
bool CheckAccountLockout() const {
166+
return (Self->AccountLockout.AttemptThreshold != 0 && CurrentFailedAttemptCount >= Self->AccountLockout.AttemptThreshold);
167+
}
88168

89-
} else {
90-
result->Record.SetError("Login authentication is disabled");
169+
bool ShouldResetFailedAttemptCount(const TInstant& lastFailedAttempt) {
170+
if (Self->AccountLockout.AttemptResetDuration == TDuration::Zero()) {
171+
return false;
91172
}
173+
return lastFailedAttempt + Self->AccountLockout.AttemptResetDuration < TAppData::TimeProvider->Now();
174+
}
92175

93-
LOG_DEBUG_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD,
94-
"TTxLogin DoComplete"
95-
<< ", result: " << result->Record.ShortDebugString()
96-
<< ", at schemeshard: " << Self->TabletID());
176+
bool ShouldUnlockAccount(const TInstant& lastFailedAttempt) {
177+
return ShouldResetFailedAttemptCount(lastFailedAttempt);
178+
}
179+
180+
void ResetFailedAttemptCount(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db) {
181+
db.Table<Schema::LoginSids>().Key(loginRequest.User).Update<Schema::LoginSids::FailedAttemptCount>(Schema::LoginSids::FailedAttemptCount::Default);
182+
CurrentFailedAttemptCount = Schema::LoginSids::FailedAttemptCount::Default;
183+
}
184+
185+
void UnlockAccount(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, NIceDb::TNiceDb& db) {
186+
ResetFailedAttemptCount(loginRequest, db);
187+
}
97188

98-
ctx.Send(Request->Sender, std::move(result), 0, Request->Cookie);
189+
void HandleLoginAuthSuccess(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, const NLogin::TLoginProvider::TLoginUserResponse& loginResponse, NIceDb::TNiceDb& db) {
190+
db.Table<Schema::LoginSids>().Key(loginRequest.User).Update<Schema::LoginSids::LastSuccessfulAttempt, Schema::LoginSids::FailedAttemptCount>(TAppData::TimeProvider->Now().MicroSeconds(), Schema::LoginSids::FailedAttemptCount::Default);
99191
}
100192

193+
void HandleLoginAuthInvalidPassword(const NLogin::TLoginProvider::TLoginUserRequest& loginRequest, const NLogin::TLoginProvider::TLoginUserResponse& loginResponse, NIceDb::TNiceDb& db) {
194+
db.Table<Schema::LoginSids>().Key(loginRequest.User).Update<Schema::LoginSids::LastFailedAttempt, Schema::LoginSids::FailedAttemptCount>(TAppData::TimeProvider->Now().MicroSeconds(), CurrentFailedAttemptCount + 1);
195+
}
101196
};
102197

103198
NTabletFlatExecutor::ITransaction* TSchemeShard::CreateTxLogin(TEvSchemeShard::TEvLogin::TPtr &ev) {

ydb/core/tx/schemeshard/schemeshard_impl.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4438,6 +4438,20 @@ TActorId TSchemeShard::TPipeClientFactory::CreateClient(const TActorContext& ctx
44384438
return clientId;
44394439
}
44404440

4441+
TSchemeShard::TAccountLockout::TAccountLockout(const ::NKikimrProto::TAccountLockout& accountLockout)
4442+
: AttemptThreshold(accountLockout.GetAttemptThreshold())
4443+
{
4444+
AttemptResetDuration = TDuration::Zero();
4445+
if (accountLockout.GetAttemptResetDuration().empty()) {
4446+
return;
4447+
}
4448+
if (TDuration::TryParse(accountLockout.GetAttemptResetDuration(), AttemptResetDuration)) {
4449+
if (AttemptResetDuration.Seconds() == 0) {
4450+
AttemptResetDuration = TDuration::Zero();
4451+
}
4452+
}
4453+
}
4454+
44414455
TSchemeShard::TSchemeShard(const TActorId &tablet, TTabletStorageInfo *info)
44424456
: TActor(&TThis::StateInit)
44434457
, TTabletExecutedFlat(info, tablet, new NMiniKQL::TMiniKQLFactory)
@@ -4476,6 +4490,7 @@ TSchemeShard::TSchemeShard(const TActorId &tablet, TTabletStorageInfo *info)
44764490
.SpecialChars = AppData()->AuthConfig.GetPasswordComplexity().GetSpecialChars(),
44774491
.CanContainUsername = AppData()->AuthConfig.GetPasswordComplexity().GetCanContainUsername()
44784492
}))
4493+
, AccountLockout(AppData()->AuthConfig.GetAccountLockout())
44794494
{
44804495
TabletCountersPtr.Reset(new TProtobufTabletCounters<
44814496
ESimpleCounters_descriptor,
@@ -7162,6 +7177,7 @@ void TSchemeShard::ApplyConsoleConfigs(const NKikimrConfig::TAppConfig& appConfi
71627177

71637178
if (appConfig.HasAuthConfig()) {
71647179
ConfigureLoginProvider(appConfig.GetAuthConfig(), ctx);
7180+
ConfigureAccountLockout(appConfig.GetAuthConfig(), ctx);
71657181
}
71667182

71677183
if (IsSchemeShardConfigured()) {
@@ -7383,6 +7399,17 @@ void TSchemeShard::ConfigureLoginProvider(
73837399
<< ", CanContainUsername# " << (passwordComplexity.CanContainUsername ? "true" : "false"));
73847400
}
73857401

7402+
void TSchemeShard::ConfigureAccountLockout(
7403+
const ::NKikimrProto::TAuthConfig& config,
7404+
const TActorContext &ctx)
7405+
{
7406+
AccountLockout = TAccountLockout(config.GetAccountLockout());
7407+
7408+
LOG_NOTICE_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD,
7409+
"AccountLockout configured: AttemptThreshold# " << AccountLockout.AttemptThreshold
7410+
<< ", AttemptResetDuration# " << AccountLockout.AttemptResetDuration.ToString());
7411+
}
7412+
73867413
void TSchemeShard::StartStopCompactionQueues() {
73877414
// note, that we don't need to check current state of compaction queue
73887415
if (IsServerlessDomain(TPath::Init(RootPathId(), this))) {

ydb/core/tx/schemeshard/schemeshard_impl.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
#include <ydb/core/protos/counters_schemeshard.pb.h>
3737
#include <ydb/core/protos/filestore_config.pb.h>
3838
#include <ydb/core/protos/flat_scheme_op.pb.h>
39+
#include <ydb/core/protos/auth.pb.h>
3940
#include <ydb/core/sys_view/common/events.h>
4041
#include <ydb/core/statistics/events.h>
4142
#include <ydb/core/tablet/pipe_tracker.h>
@@ -500,6 +501,10 @@ class TSchemeShard
500501
const ::NKikimrProto::TAuthConfig& config,
501502
const TActorContext &ctx);
502503

504+
void ConfigureAccountLockout(
505+
const ::NKikimrProto::TAuthConfig& config,
506+
const TActorContext &ctx);
507+
503508
void StartStopCompactionQueues();
504509

505510
void WaitForTableProfiles(ui64 importId, ui32 itemIdx);
@@ -1463,6 +1468,15 @@ class TSchemeShard
14631468

14641469
NLogin::TLoginProvider LoginProvider;
14651470

1471+
struct TAccountLockout {
1472+
size_t AttemptThreshold = 4;
1473+
TDuration AttemptResetDuration = TDuration::Hours(1);
1474+
1475+
TAccountLockout(const ::NKikimrProto::TAccountLockout& accountLockout);
1476+
};
1477+
1478+
TAccountLockout AccountLockout;
1479+
14661480
private:
14671481
void OnDetach(const TActorContext &ctx) override;
14681482
void OnTabletDead(TEvTablet::TEvTabletDead::TPtr &ev, const TActorContext &ctx) override;

ydb/core/tx/schemeshard/schemeshard_schema.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1628,9 +1628,19 @@ struct Schema : NIceDb::Schema {
16281628
struct SidName : Column<1, NScheme::NTypeIds::String> {};
16291629
struct SidType : Column<2, NScheme::NTypeIds::Uint64> { using Type = NLoginProto::ESidType::SidType; };
16301630
struct SidHash : Column<3, NScheme::NTypeIds::String> {};
1631+
struct LastSuccessfulAttempt : Column<4, NScheme::NTypeIds::Timestamp> {};
1632+
struct LastFailedAttempt : Column<5, NScheme::NTypeIds::Timestamp> {};
1633+
struct FailedAttemptCount : Column<6, NScheme::NTypeIds::Uint32> {using Type = ui32; static constexpr Type Default = 0;};
16311634

16321635
using TKey = TableKey<SidName>;
1633-
using TColumns = TableColumns<SidName, SidType, SidHash>;
1636+
using TColumns = TableColumns<
1637+
SidName,
1638+
SidType,
1639+
SidHash,
1640+
LastSuccessfulAttempt,
1641+
LastFailedAttempt,
1642+
FailedAttemptCount
1643+
>;
16341644
};
16351645

16361646
struct LoginSidMembers : Table<94> {

ydb/core/tx/schemeshard/ut_helpers/helpers.cpp

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <ydb/core/util/pb.h>
1818
#include <ydb/public/api/protos/ydb_export.pb.h>
1919
#include <ydb/core/protos/schemeshard/operations.pb.h>
20+
#include <ydb/core/protos/auth.pb.h>
2021
#include <ydb/public/sdk/cpp/client/ydb_table/table.h>
2122

2223
#include <library/cpp/testing/unittest/registar.h>
@@ -1976,14 +1977,25 @@ namespace NSchemeShardUT_Private {
19761977
auto transaction = modifyTx->Record.AddTransaction();
19771978
transaction->SetWorkingDir(database);
19781979
transaction->SetOperationType(NKikimrSchemeOp::EOperationType::ESchemeOpAlterLogin);
1979-
19801980
auto removeUser = transaction->MutableAlterLogin()->MutableRemoveUser();
19811981
removeUser->SetUser(user);
19821982

19831983
AsyncSend(runtime, TTestTxConfig::SchemeShard, modifyTx.release());
19841984
TestModificationResults(runtime, txId, expectedResults);
19851985
}
19861986

1987+
void CreateAlterLoginCreateGroup(TTestActorRuntime& runtime, ui64 txId, const TString& database, const TString& group, const TVector<TExpectedResult>& expectedResults) {
1988+
auto modifyTx = std::make_unique<TEvSchemeShard::TEvModifySchemeTransaction>(txId, TTestTxConfig::SchemeShard);
1989+
auto transaction = modifyTx->Record.AddTransaction();
1990+
transaction->SetWorkingDir(database);
1991+
transaction->SetOperationType(NKikimrSchemeOp::EOperationType::ESchemeOpAlterLogin);
1992+
auto createGroup = transaction->MutableAlterLogin()->MutableCreateGroup();
1993+
createGroup->SetGroup(group);
1994+
1995+
AsyncSend(runtime, TTestTxConfig::SchemeShard, modifyTx.release());
1996+
TestModificationResults(runtime, txId, expectedResults);
1997+
}
1998+
19871999
void AlterLoginAddGroupMembership(TTestActorRuntime& runtime, ui64 txId, const TString& database, const TString& member, const TString& group, const TVector<TExpectedResult>& expectedResults) {
19882000
auto modifyTx = std::make_unique<TEvSchemeShard::TEvModifySchemeTransaction>(txId, TTestTxConfig::SchemeShard);
19892001
auto transaction = modifyTx->Record.AddTransaction();
@@ -2010,13 +2022,17 @@ namespace NSchemeShardUT_Private {
20102022

20112023
AsyncSend(runtime, TTestTxConfig::SchemeShard, modifyTx.release());
20122024
TestModificationResults(runtime, txId, expectedResults);
2013-
}
2014-
2025+
}
2026+
20152027
NKikimrScheme::TEvLoginResult Login(TTestActorRuntime& runtime, const TString& user, const TString& password) {
20162028
TActorId sender = runtime.AllocateEdgeActor();
20172029
auto evLogin = new TEvSchemeShard::TEvLogin();
20182030
evLogin->Record.SetUser(user);
20192031
evLogin->Record.SetPassword(password);
2032+
2033+
if (auto ldapDomain = runtime.GetAppData().AuthConfig.GetLdapAuthenticationDomain(); user.EndsWith("@" + ldapDomain)) {
2034+
evLogin->Record.SetExternalAuth(ldapDomain);
2035+
}
20202036
ForwardToTablet(runtime, TTestTxConfig::SchemeShard, sender, evLogin);
20212037
TAutoPtr<IEventHandle> handle;
20222038
auto event = runtime.GrabEdgeEvent<TEvSchemeShard::TEvLoginResult>(handle);

0 commit comments

Comments
 (0)