Skip to content

Commit

Permalink
Automatically account lockout after some attempts to login with wrong…
Browse files Browse the repository at this point in the history
… password (ydb-platform#12578)
  • Loading branch information
molotkov-and authored Dec 20, 2024
1 parent 4ca7f5c commit bc46b2c
Show file tree
Hide file tree
Showing 13 changed files with 760 additions and 123 deletions.
6 changes: 6 additions & 0 deletions ydb/core/protos/auth.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"];
}
139 changes: 117 additions & 22 deletions ydb/core/tx/schemeshard/schemeshard__login.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{}

Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -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) {
Expand Down
27 changes: 27 additions & 0 deletions ydb/core/tx/schemeshard/schemeshard_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -7162,6 +7177,7 @@ void TSchemeShard::ApplyConsoleConfigs(const NKikimrConfig::TAppConfig& appConfi

if (appConfig.HasAuthConfig()) {
ConfigureLoginProvider(appConfig.GetAuthConfig(), ctx);
ConfigureAccountLockout(appConfig.GetAuthConfig(), ctx);
}

if (IsSchemeShardConfigured()) {
Expand Down Expand Up @@ -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))) {
Expand Down
14 changes: 14 additions & 0 deletions ydb/core/tx/schemeshard/schemeshard_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -500,6 +501,10 @@ class TSchemeShard
const ::NKikimrProto::TAuthConfig& config,
const TActorContext &ctx);

void ConfigureAccountLockout(
const ::NKikimrProto::TAuthConfig& config,
const TActorContext &ctx);

void StartStopCompactionQueues();

void WaitForTableProfiles(ui64 importId, ui32 itemIdx);
Expand Down Expand Up @@ -1463,6 +1468,15 @@ 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;
Expand Down
12 changes: 11 additions & 1 deletion ydb/core/tx/schemeshard/schemeshard_schema.h
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
22 changes: 19 additions & 3 deletions ydb/core/tx/schemeshard/ut_helpers/helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -1976,14 +1977,25 @@ namespace NSchemeShardUT_Private {
auto transaction = modifyTx->Record.AddTransaction();
transaction->SetWorkingDir(database);
transaction->SetOperationType(NKikimrSchemeOp::EOperationType::ESchemeOpAlterLogin);

auto removeUser = transaction->MutableAlterLogin()->MutableRemoveUser();
removeUser->SetUser(user);

AsyncSend(runtime, TTestTxConfig::SchemeShard, modifyTx.release());
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();
Expand All @@ -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);
Expand Down
Loading

0 comments on commit bc46b2c

Please sign in to comment.