summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrey Zaspa <[email protected]>2026-01-16 10:55:29 +0400
committerGitHub <[email protected]>2026-01-16 10:55:29 +0400
commit60e4cc5f9cd154bf2e686ec96d015ad875d09386 (patch)
tree24895c01f156ec2797888ff14933a35fa7cc4bb2
parentebc20a6541ab2bd9b6699a0e26a244b7dc40d4d2 (diff)
Compute password hashes on nodes processing Login requests (#31137)
-rw-r--r--ydb/core/grpc_services/grpc_request_proxy.cpp15
-rw-r--r--ydb/core/grpc_services/rpc_login.cpp151
-rw-r--r--ydb/core/grpc_services/ya.make1
-rw-r--r--ydb/core/security/login_shared_func.cpp95
-rw-r--r--ydb/core/security/login_shared_func.h35
-rw-r--r--ydb/core/security/sasl/base_auth_actors.cpp220
-rw-r--r--ydb/core/security/sasl/base_auth_actors.h82
-rw-r--r--ydb/core/security/sasl/events.h18
-rw-r--r--ydb/core/security/sasl/plain_auth_actor.cpp261
-rw-r--r--ydb/core/security/sasl/plain_auth_actor.h10
-rw-r--r--ydb/core/security/sasl/plain_ldap_auth_proxy_actor.cpp111
-rw-r--r--ydb/core/security/sasl/plain_ldap_auth_proxy_actor.h10
-rw-r--r--ydb/core/security/sasl/static_credentials_provider.cpp102
-rw-r--r--ydb/core/security/sasl/static_credentials_provider.h63
-rw-r--r--ydb/core/security/sasl/ya.make8
-rw-r--r--ydb/core/viewer/tests/canondata/result.json4
-rw-r--r--ydb/core/viewer/tests/test.py1
-rw-r--r--ydb/library/login/login.cpp8
-rw-r--r--ydb/library/login/protos/login.proto6
-rw-r--r--ydb/library/login/sasl/scram.cpp51
-rw-r--r--ydb/library/login/sasl/scram.h7
-rw-r--r--ydb/services/ydb/ydb_common_ut.h1
22 files changed, 1090 insertions, 170 deletions
diff --git a/ydb/core/grpc_services/grpc_request_proxy.cpp b/ydb/core/grpc_services/grpc_request_proxy.cpp
index a3e4f89730f..4117d0c6d7d 100644
--- a/ydb/core/grpc_services/grpc_request_proxy.cpp
+++ b/ydb/core/grpc_services/grpc_request_proxy.cpp
@@ -10,6 +10,7 @@
#include <ydb/core/cms/console/configs_dispatcher.h>
#include <ydb/core/cms/console/console.h>
#include <ydb/core/grpc_services/counters/proxy_counters.h>
+#include <ydb/core/security/sasl/static_credentials_provider.h>
#include <ydb/core/tx/tx_proxy/proxy.h>
#include <ydb/core/tx/scheme_board/scheme_board.h>
#include <ydb/library/wilson_ids/wilson.h>
@@ -549,12 +550,15 @@ void TGRpcRequestProxyImpl::HandleSchemeBoard(TSchemeBoardEvents::TEvNotifyUpdat
}
if (describeScheme.GetPathDescription().HasDomainDescription()
- && describeScheme.GetPathDescription().GetDomainDescription().HasSecurityState()
- && describeScheme.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize() > 0) {
+ && describeScheme.GetPathDescription().GetDomainDescription().HasSecurityState())
+ {
+ const auto& securityState = describeScheme.GetPathDescription().GetDomainDescription().GetSecurityState();
LOG_DEBUG_S(*TlsActivationContext, NKikimrServices::GRPC_SERVER, "Updating SecurityState for " << databaseName);
- Send(MakeTicketParserID(), new TEvTicketParser::TEvUpdateLoginSecurityState(
- describeScheme.GetPathDescription().GetDomainDescription().GetSecurityState()
- ));
+ NSasl::TStaticCredentialsProvider::GetInstance().UpdateDatabaseUsers(securityState);
+
+ if (securityState.PublicKeysSize() > 0) {
+ Send(MakeTicketParserID(), new TEvTicketParser::TEvUpdateLoginSecurityState(securityState));
+ }
} else {
if (!describeScheme.GetPathDescription().HasDomainDescription()) {
LOG_DEBUG_S(*TlsActivationContext, NKikimrServices::GRPC_SERVER, "Can't update SecurityState for " << databaseName << " - no DomainDescription");
@@ -597,6 +601,7 @@ void TGRpcRequestProxyImpl::ForgetDatabase(const TString& database) {
DeferredEvents.erase(itDeferredEvents);
}
Databases.erase(database);
+ NSasl::TStaticCredentialsProvider::GetInstance().DeleteDatabaseUsers(database);
}
void TGRpcRequestProxyImpl::SubscribeToDatabase(const TString& database) {
diff --git a/ydb/core/grpc_services/rpc_login.cpp b/ydb/core/grpc_services/rpc_login.cpp
index cd318ddd7ec..093b29040ed 100644
--- a/ydb/core/grpc_services/rpc_login.cpp
+++ b/ydb/core/grpc_services/rpc_login.cpp
@@ -11,43 +11,49 @@
#include <ydb/core/security/ldap_auth_provider/ldap_auth_provider.h>
#include <ydb/core/security/login_shared_func.h>
+#include <ydb/core/security/sasl/events.h>
+#include <ydb/core/security/sasl/plain_auth_actor.h>
+#include <ydb/core/security/sasl/plain_ldap_auth_proxy_actor.h>
#include <ydb/library/aclib/aclib.h>
#include <ydb/library/login/login.h>
+#include <ydb/library/login/sasl/scram.h>
#include <ydb/public/api/protos/ydb_auth.pb.h>
namespace NKikimr {
namespace NGRpcService {
+using namespace NSasl;
using namespace NSchemeShard;
using TEvLoginRequest = TGRpcRequestWrapperNoAuth<TRpcServices::EvLogin, Ydb::Auth::LoginRequest, Ydb::Auth::LoginResponse>;
class TLoginRPC : public TRpcRequestActor<TLoginRPC, TEvLoginRequest, true> {
private:
- TAuthCredentials Credentials;
TString PathToDatabase;
public:
using TRpcRequestActor::TRpcRequestActor;
TDuration Timeout = TDuration::MilliSeconds(60000);
- TActorId PipeClient;
- NTabletPipe::TClientConfig GetPipeClientConfig() {
- NTabletPipe::TClientConfig clientConfig;
- clientConfig.RetryPolicy = {.RetryLimitCount = 3};
- return clientConfig;
- }
-
- void Bootstrap() {
- const Ydb::Auth::LoginRequest* protoRequest = GetProtoRequest();
- Credentials = PrepareCredentials(protoRequest->user(), protoRequest->password(), AppData()->AuthConfig);
+ void Bootstrap(const TActorContext &ctx) {
TString domainName = "/" + AppData()->DomainsInfo->GetDomain()->Name;
PathToDatabase = AppData()->AuthConfig.GetDomainLoginOnly() ? domainName : GetDatabaseName();
- auto sendParameters = GetSendParameters(Credentials, PathToDatabase);
- Send(sendParameters.Recipient, sendParameters.Event.Release());
+
+ std::unique_ptr<IActor> authActor;
+ const Ydb::Auth::LoginRequest* protoRequest = GetProtoRequest();
+ if (IsUsernameFromLdapAuthDomain(protoRequest->user(), AppData()->AuthConfig)) {
+ const TString ldapUsername = PrepareLdapUsername(protoRequest->user(), AppData()->AuthConfig);
+ const TString saslPlainAuthMsg = NLogin::NSasl::PrepareSaslPlainAuthMsg(ldapUsername, protoRequest->password());
+ authActor = CreatePlainLdapAuthProxyActor(ctx.SelfID, PathToDatabase, saslPlainAuthMsg, Request->GetPeerName());
+ } else {
+ const TString saslPlainAuthMsg = NLogin::NSasl::PrepareSaslPlainAuthMsg(protoRequest->user(), protoRequest->password());
+ authActor = CreatePlainAuthActor(ctx.SelfID, PathToDatabase, saslPlainAuthMsg, Request->GetPeerName());
+ }
+
+ Register(authActor.release());
Become(&TThis::StateWork, Timeout, new TEvents::TEvWakeup());
}
@@ -55,77 +61,52 @@ public:
ReplyErrorAndPassAway(Ydb::StatusIds::TIMEOUT, "Login timeout");
}
- void HandleNavigate(TEvTxProxySchemeCache::TEvNavigateKeySetResult::TPtr& ev) {
- const auto& resultSet = ev->Get()->Request.Get()->ResultSet;
- if (resultSet.size() == 1 && resultSet.front().Status == NSchemeCache::TSchemeCacheNavigate::EStatus::Ok) {
- const auto domainInfo = resultSet.front().DomainInfo;
- if (domainInfo != nullptr) {
- IActor* pipe = NTabletPipe::CreateClient(SelfId(), domainInfo->ExtractSchemeShard(), GetPipeClientConfig());
- PipeClient = RegisterWithSameMailbox(pipe);
- THolder<TEvSchemeShard::TEvLogin> request = MakeHolder<TEvSchemeShard::TEvLogin>();
- request.Get()->Record = CreateLoginRequest(Credentials, AppData()->AuthConfig);
- request.Get()->Record.SetPeerName(Request->GetPeerName());
- NTabletPipe::SendData(SelfId(), PipeClient, request.Release());
- return;
- }
- }
- ReplyErrorAndPassAway(Ydb::StatusIds::SCHEME_ERROR, "No database found");
- }
-
- void Handle(TEvLdapAuthProvider::TEvAuthenticateResponse::TPtr& ev) {
- const TEvLdapAuthProvider::TEvAuthenticateResponse& response = *ev->Get();
- if (response.Status == TEvLdapAuthProvider::EStatus::SUCCESS) {
- Send(MakeSchemeCacheID(), new TEvTxProxySchemeCache::TEvNavigateKeySet(CreateNavigateKeySetRequest(PathToDatabase).Release()));
- } else {
- ReplyErrorAndPassAway(ConvertLdapStatus(response.Status), response.Error.Message, response.Error.LogMessage);
- }
- }
-
- void HandleResult(TEvSchemeShard::TEvLoginResult::TPtr& ev) {
- const NKikimrScheme::TEvLoginResult& loginResult = ev->Get()->Record;
- if (loginResult.error()) {
- // explicit error takes precedence
- ReplyErrorAndPassAway(Ydb::StatusIds::UNAUTHORIZED, loginResult.error(), /*loginResult.details()*/ TString());
- } else if (loginResult.token().empty()) {
- // empty token is still an error
- ReplyErrorAndPassAway(Ydb::StatusIds::INTERNAL_ERROR, "Failed to produce a token");
- } else {
- // success = token + no errors
- ReplyAndPassAway(loginResult);
+ void ProcessLoginResult(const NYql::TIssue& issue, const std::string& reason, const std::string& token,
+ const std::string& sanitizedToken, bool isAdmin)
+ {
+ switch (issue.GetCode()) {
+ case NKikimrIssues::TIssuesIds::SUCCESS:
+ return ReplyAndPassAway(token, sanitizedToken, isAdmin);
+ case NKikimrIssues::TIssuesIds::ACCESS_DENIED:
+ return ReplyErrorAndPassAway(Ydb::StatusIds::UNAUTHORIZED, issue.GetMessage(), TString(reason));
+ case NKikimrIssues::TIssuesIds::UNEXPECTED:
+ return ReplyErrorAndPassAway(Ydb::StatusIds::INTERNAL_ERROR, issue.GetMessage());
+ case NKikimrIssues::TIssuesIds::DATABASE_NOT_EXIST:
+ return ReplyErrorAndPassAway(Ydb::StatusIds::SCHEME_ERROR, "No database found");
+ case NKikimrIssues::TIssuesIds::SHARD_NOT_AVAILABLE:
+ return ReplyErrorAndPassAway(Ydb::StatusIds::UNAVAILABLE, "SchemeShard is unreachable");
+ case NKikimrIssues::TIssuesIds::DEFAULT_ERROR:
+ return ReplyErrorAndPassAway(Ydb::StatusIds::INTERNAL_ERROR, issue.GetMessage());
+ case NKikimrIssues::TIssuesIds::YDB_AUTH_UNAVAILABLE:
+ return ReplyErrorAndPassAway(Ydb::StatusIds::UNAVAILABLE, issue.GetMessage(), TString(reason));
+ case NKikimrIssues::TIssuesIds::YDB_API_VALIDATION_ERROR:
+ return ReplyErrorAndPassAway(Ydb::StatusIds::BAD_REQUEST, issue.GetMessage(), TString(reason));
+ default:
+ return ReplyErrorAndPassAway(Ydb::StatusIds::GENERIC_ERROR, issue.GetMessage());
}
}
- void HandleUndelivered(TEvents::TEvUndelivered::TPtr&) {
- ReplyErrorAndPassAway(Ydb::StatusIds::UNAVAILABLE, "SchemeShard is unreachable");
+ void HandleResult(TEvSasl::TEvSaslPlainLoginResponse::TPtr& ev) {
+ const auto& loginResult = *ev->Get();
+ ProcessLoginResult(loginResult.Issue, "", loginResult.Token, loginResult.SanitizedToken,
+ loginResult.IsAdmin);
}
- void HandleConnect(TEvTabletPipe::TEvClientConnected::TPtr& ev) {
- if (ev->Get()->Status != NKikimrProto::OK) {
- ReplyErrorAndPassAway(Ydb::StatusIds::UNAVAILABLE, "SchemeShard is unavailable");
- }
- }
-
- void HandleDestroyed(TEvTabletPipe::TEvClientDestroyed::TPtr&) {
- ReplyErrorAndPassAway(Ydb::StatusIds::UNAVAILABLE, "SchemeShard is unavailable");
+ void HandleResult(TEvSasl::TEvSaslPlainLdapLoginResponse::TPtr& ev) {
+ const auto& loginResult = *ev->Get();
+ ProcessLoginResult(loginResult.Issue, loginResult.Reason, loginResult.Token, loginResult.SanitizedToken,
+ loginResult.IsAdmin);
}
STATEFN(StateWork) {
switch (ev->GetTypeRewrite()) {
- hFunc(TEvents::TEvUndelivered, HandleUndelivered);
- hFunc(TEvTabletPipe::TEvClientConnected, HandleConnect);
- hFunc(TEvTabletPipe::TEvClientDestroyed, HandleDestroyed);
- hFunc(TEvTxProxySchemeCache::TEvNavigateKeySetResult, HandleNavigate);
- hFunc(TEvSchemeShard::TEvLoginResult, HandleResult);
- hFunc(TEvLdapAuthProvider::TEvAuthenticateResponse, Handle);
+ hFunc(TEvSasl::TEvSaslPlainLoginResponse, HandleResult);
+ hFunc(TEvSasl::TEvSaslPlainLdapLoginResponse, HandleResult);
cFunc(TEvents::TSystem::Wakeup, HandleTimeout);
}
}
- void ReplyAndPassAway(const NKikimrScheme::TEvLoginResult& loginResult) {
- const auto& resultToken = loginResult.GetToken();
- const auto& sanitizedToken = loginResult.GetSanitizedToken();
- const auto& isAdmin = loginResult.GetIsAdmin();
-
+ void ReplyAndPassAway(const std::string& token, const std::string& sanitizedToken, bool isAdmin) {
TResponse response;
Ydb::Operations::Operation& operation = *response.mutable_operation();
operation.set_ready(true);
@@ -133,13 +114,14 @@ public:
// Pack result to google::protobuf::Any
{
Ydb::Auth::LoginResult result;
- result.set_token(resultToken);
+ result.set_token(token);
operation.mutable_result()->PackFrom(result);
}
- AuditLogLogin(Request.Get(), PathToDatabase, *GetProtoRequest(), response, /* errorDetails */ TString(), sanitizedToken, isAdmin);
+ AuditLogLogin(Request.Get(), PathToDatabase, *GetProtoRequest(), response,
+ /* errorDetails */ "", TString(sanitizedToken), isAdmin);
- return CleanupAndReply(response);
+ return Reply(response);
}
void ReplyErrorAndPassAway(const Ydb::StatusIds_StatusCode status, const TString& error, const TString& reason = "") {
@@ -156,30 +138,9 @@ public:
AuditLogLogin(Request.Get(), PathToDatabase, *GetProtoRequest(), response, reason, {});
- return CleanupAndReply(response);
- }
-
- void CleanupAndReply(const TResponse& response) {
- if (PipeClient) {
- NTabletPipe::CloseClient(SelfId(), PipeClient);
- }
-
return Reply(response);
}
-private:
- static Ydb::StatusIds::StatusCode ConvertLdapStatus(const TEvLdapAuthProvider::EStatus& status) {
- switch (status) {
- case NKikimr::TEvLdapAuthProvider::EStatus::SUCCESS:
- return Ydb::StatusIds::SUCCESS;
- case NKikimr::TEvLdapAuthProvider::EStatus::UNAUTHORIZED:
- return Ydb::StatusIds::UNAUTHORIZED;
- case NKikimr::TEvLdapAuthProvider::EStatus::UNAVAILABLE:
- return Ydb::StatusIds::UNAVAILABLE;
- case NKikimr::TEvLdapAuthProvider::EStatus::BAD_REQUEST:
- return Ydb::StatusIds::BAD_REQUEST;
- }
- }
};
void DoLoginRequest(std::unique_ptr<IRequestOpCtx> p, const IFacilityProvider& f) {
diff --git a/ydb/core/grpc_services/ya.make b/ydb/core/grpc_services/ya.make
index ce0afbf8d7b..9e71e6ea48a 100644
--- a/ydb/core/grpc_services/ya.make
+++ b/ydb/core/grpc_services/ya.make
@@ -147,6 +147,7 @@ PEERDIR(
ydb/core/ydb_convert
ydb/core/security
ydb/core/security/ldap_auth_provider
+ ydb/core/security/sasl
ydb/library/aclib
yql/essentials/types/binary_json
yql/essentials/types/dynumber
diff --git a/ydb/core/security/login_shared_func.cpp b/ydb/core/security/login_shared_func.cpp
index f013534bf82..ddc0cb012f7 100644
--- a/ydb/core/security/login_shared_func.cpp
+++ b/ydb/core/security/login_shared_func.cpp
@@ -1,64 +1,71 @@
-#include <util/generic/string.h>
-#include <ydb/core/tx/scheme_cache/scheme_cache.h>
-#include <ydb/core/base/appdata_fwd.h>
-#include <ydb/core/base/path.h>
-#include <ydb/core/security/ldap_auth_provider/ldap_auth_provider.h>
#include "login_shared_func.h"
-namespace NKikimr {
+#include <util/generic/string.h>
-THolder<NSchemeCache::TSchemeCacheNavigate> CreateNavigateKeySetRequest(const TString& pathToDatabase) {
- auto request = MakeHolder<NSchemeCache::TSchemeCacheNavigate>();
- request->DatabaseName = pathToDatabase;
- auto& entry = request->ResultSet.emplace_back();
- entry.Operation = NSchemeCache::TSchemeCacheNavigate::OpPath;
- entry.Path = ::NKikimr::SplitPath(pathToDatabase);
- entry.RedirectRequired = false;
- return request;
-}
+#include <ydb/core/protos/auth.pb.h>
+
+
+namespace NKikimr {
-TAuthCredentials PrepareCredentials(const TString& login, const TString& password, const NKikimrProto::TAuthConfig& config) {
+bool IsUsernameFromLdapAuthDomain(const TString& username, const NKikimrProto::TAuthConfig& config) {
if (config.HasLdapAuthentication() && !config.GetLdapAuthenticationDomain().empty()) {
- const TString domain = "@" + config.GetLdapAuthenticationDomain();
- if (login.EndsWith(domain)) {
- return {.AuthType = TAuthCredentials::EAuthType::Ldap, .Login = login.substr(0, login.size() - domain.size()), .Password = password};
- }
+ const TString ldapDomain = "@" + config.GetLdapAuthenticationDomain();
+ return username.EndsWith(ldapDomain);
}
- return {.AuthType = TAuthCredentials::EAuthType::Internal, .Login = login, .Password = password};
+
+ return false;
+}
+
+TString PrepareLdapUsername(const TString& username, const NKikimrProto::TAuthConfig& config) {
+ const TString ldapDomain = "@" + config.GetLdapAuthenticationDomain();
+ return username.substr(0, username.size() - ldapDomain.size());
}
-NKikimrScheme::TEvLogin CreateLoginRequest(const TAuthCredentials& credentials, const NKikimrProto::TAuthConfig& config) {
+NKikimrScheme::TEvLogin CreatePlainLoginRequest(const TString& username, NLoginProto::EHashType::HashType hashType,
+ const TString& hashToValidate, const TString& peerName, const NKikimrProto::TAuthConfig& config)
+{
NKikimrScheme::TEvLogin record;
- record.SetUser(credentials.Login);
- record.SetPassword(credentials.Password);
- switch (credentials.AuthType) {
- case TAuthCredentials::EAuthType::Ldap: {
- record.SetExternalAuth(config.GetLdapAuthenticationDomain());
- break;
- }
- default: {}
+ record.SetUser(username);
+ record.MutableHashToValidate()->SetAuthMech(NLoginProto::ESaslAuthMech::Plain);
+ record.MutableHashToValidate()->SetHashType(hashType);
+ record.MutableHashToValidate()->SetHash(hashToValidate);
+
+ if (config.HasLoginTokenExpireTime()) {
+ record.SetExpiresAfterMs(TDuration::Parse(config.GetLoginTokenExpireTime()).MilliSeconds());
}
+
+ record.SetPeerName(peerName);
+ return record;
+}
+
+NKikimrScheme::TEvLogin CreatePlainLoginRequestOldFormat(const TString& username, const TString& password,
+ const TString& peerName, const NKikimrProto::TAuthConfig& config)
+{
+ NKikimrScheme::TEvLogin record;
+ record.SetUser(username);
+ record.SetPassword(password);
+
if (config.HasLoginTokenExpireTime()) {
record.SetExpiresAfterMs(TDuration::Parse(config.GetLoginTokenExpireTime()).MilliSeconds());
}
+
+ record.SetPeerName(peerName);
return record;
}
-TSendParameters GetSendParameters(const TAuthCredentials& credentials, const TString& pathToDatabase) {
- switch (credentials.AuthType) {
- case TAuthCredentials::EAuthType::Internal: {
- return {
- .Recipient = MakeSchemeCacheID(),
- .Event = MakeHolder<TEvTxProxySchemeCache::TEvNavigateKeySet>(CreateNavigateKeySetRequest(pathToDatabase).Release())
- };
- }
- case TAuthCredentials::EAuthType::Ldap: {
- return {
- .Recipient = MakeLdapAuthProviderID(),
- .Event = MakeHolder<TEvLdapAuthProvider::TEvAuthenticateRequest>(credentials.Login, credentials.Password)
- };
- }
+NKikimrScheme::TEvLogin CreatePlainLdapLoginRequest(const TString& username, const TString& peerName,
+ const NKikimrProto::TAuthConfig& config)
+{
+ NKikimrScheme::TEvLogin record;
+ record.SetUser(username);
+ record.SetExternalAuth(config.GetLdapAuthenticationDomain());
+
+ if (config.HasLoginTokenExpireTime()) {
+ record.SetExpiresAfterMs(TDuration::Parse(config.GetLoginTokenExpireTime()).MilliSeconds());
}
+
+ record.SetPeerName(peerName);
+ return record;
}
} // namespace NKikimr
diff --git a/ydb/core/security/login_shared_func.h b/ydb/core/security/login_shared_func.h
index b7d4e60146c..b17c4e59e06 100644
--- a/ydb/core/security/login_shared_func.h
+++ b/ydb/core/security/login_shared_func.h
@@ -2,33 +2,20 @@
#include <util/generic/string.h>
#include <util/generic/ptr.h>
-#include <ydb/core/tx/scheme_cache/scheme_cache.h>
-namespace NKikimrProto {
- class TAuthConfig;
-};
+#include <ydb/core/base/appdata_fwd.h>
+#include <ydb/core/protos/flat_tx_scheme.pb.h>
+#include <ydb/library/login/protos/login.pb.h>
namespace NKikimr {
-struct TAuthCredentials {
- enum class EAuthType {
- Internal,
- Ldap
- };
-
- EAuthType AuthType = EAuthType::Internal;
- TString Login;
- TString Password;
-};
-
-struct TSendParameters {
- TActorId Recipient;
- THolder<IEventBase> Event;
-};
-
-THolder<NSchemeCache::TSchemeCacheNavigate> CreateNavigateKeySetRequest(const TString& pathToDatabase);
-TAuthCredentials PrepareCredentials(const TString& login, const TString& password, const NKikimrProto::TAuthConfig& config);
-NKikimrScheme::TEvLogin CreateLoginRequest(const TAuthCredentials& credentials, const NKikimrProto::TAuthConfig& config);
-TSendParameters GetSendParameters(const TAuthCredentials& credentials, const TString& pathToDatabase);
+bool IsUsernameFromLdapAuthDomain(const TString& username, const NKikimrProto::TAuthConfig& config);
+TString PrepareLdapUsername(const TString& username, const NKikimrProto::TAuthConfig& config);
+NKikimrScheme::TEvLogin CreatePlainLoginRequest(const TString& username, NLoginProto::EHashType::HashType hashType,
+ const TString& hashToValidate, const TString& peerName, const NKikimrProto::TAuthConfig& config);
+NKikimrScheme::TEvLogin CreatePlainLoginRequestOldFormat(const TString& username, const TString& password,
+ const TString& peerName, const NKikimrProto::TAuthConfig& config);
+NKikimrScheme::TEvLogin CreatePlainLdapLoginRequest(const TString& username,
+ const TString& peerName, const NKikimrProto::TAuthConfig& config);
} // namespace NKikimr
diff --git a/ydb/core/security/sasl/base_auth_actors.cpp b/ydb/core/security/sasl/base_auth_actors.cpp
new file mode 100644
index 00000000000..fb6ca607414
--- /dev/null
+++ b/ydb/core/security/sasl/base_auth_actors.cpp
@@ -0,0 +1,220 @@
+#include "base_auth_actors.h"
+
+#include <ydb/core/protos/auth.pb.h>
+
+#include <ydb/library/login/sasl/saslprep.h>
+
+namespace NKikimr::NSasl {
+
+using namespace NLogin;
+using namespace NSchemeShard;
+
+TAuthActorBase::TAuthActorBase(TActorId sender, const std::string& database, const std::string& peerName
+)
+ : Sender(sender)
+ , Database(database)
+ , PeerName(peerName)
+{
+}
+
+void TAuthActorBase::HandleNavigate(TEvTxProxySchemeCache::TEvNavigateKeySetResult::TPtr& ev, const TActorContext &ctx) {
+ LOG_TRACE_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "Handle TEvTxProxySchemeCache::TEvNavigateKeySetResult" <<
+ ", errors# " << ev->Get()->Request.Get()->ErrorCount);
+
+ const auto& resultSet = ev->Get()->Request.Get()->ResultSet;
+ if (resultSet.size() == 1 && resultSet.front().Status == NSchemeCache::TSchemeCacheNavigate::EStatus::Ok) {
+ const auto domainInfo = resultSet.front().DomainInfo;
+ if (domainInfo != nullptr) {
+ IActor* pipe = NTabletPipe::CreateClient(SelfId(), domainInfo->ExtractSchemeShard(), GetPipeClientConfig());
+ PipeClient = RegisterWithSameMailbox(pipe);
+
+ auto request = std::make_unique<TEvSchemeShard::TEvLogin>();
+ request.get()->Record = CreateLoginRequest();
+ NTabletPipe::SendData(SelfId(), PipeClient, request.release());
+ Become(&TThis::StateLogin, Timeout, new TEvents::TEvWakeup());
+ return;
+ }
+ }
+
+ std::string error = "Unknown database";
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::DATABASE_NOT_EXIST, error);
+ return CleanupAndDie(ctx);
+}
+
+void TAuthActorBase::HandleUndelivered(TEvents::TEvUndelivered::TPtr&, const TActorContext &ctx) {
+ std::string error = "SchemeShard is unreachable";
+ LOG_ERROR_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::SHARD_NOT_AVAILABLE, error);
+ return CleanupAndDie(ctx);
+}
+
+void TAuthActorBase::HandleDestroyed(TEvTabletPipe::TEvClientDestroyed::TPtr&, const TActorContext &ctx) {
+ std::string error = "SchemeShard is unreachable";
+ LOG_ERROR_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::SHARD_NOT_AVAILABLE, error);
+ return CleanupAndDie(ctx);
+}
+
+void TAuthActorBase::HandleConnect(TEvTabletPipe::TEvClientConnected::TPtr& ev, const TActorContext &ctx) {
+ if (ev->Get()->Status != NKikimrProto::OK) {
+ std::string error = "SchemeShard is unreachable";
+ LOG_ERROR_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::SHARD_NOT_AVAILABLE, error);
+ return CleanupAndDie(ctx);
+ }
+}
+
+void TAuthActorBase::HandleLoginResult(TEvSchemeShard::TEvLoginResult::TPtr& ev, const TActorContext &ctx) {
+ LOG_DEBUG_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ " Handle TEvSchemeShard::TEvLoginResult" <<
+ ", " << ev->Get()->Record.DebugString()
+ );
+
+ const NKikimrScheme::TEvLoginResult& loginResult = ev->Get()->Record;
+ if (loginResult.HasError()) { // explicit error takes precedence
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "Authentication failed: " << loginResult.GetError()
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, loginResult.GetError());
+ } else if (!loginResult.HasToken()) { // empty token is still an error
+ std::string error = "Failed to produce a token";
+ LOG_ERROR_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::DEFAULT_ERROR, error);
+ } else { // success = token + no errors
+ LOG_DEBUG_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "Authentication completed for '" << AuthcId << "'"
+ );
+
+ SendIssuedToken(loginResult);
+ }
+
+ return CleanupAndDie(ctx);
+}
+
+void TAuthActorBase::HandleTimeout(const TActorContext &ctx) {
+ std::string error = "SchemeShard response timeout";
+ LOG_ERROR_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::SHARD_NOT_AVAILABLE, error);
+ return CleanupAndDie(ctx);
+}
+
+void TAuthActorBase::ResolveSchemeShard(const TActorContext &ctx) {
+ auto request = std::make_unique<NSchemeCache::TSchemeCacheNavigate>();
+ request->DatabaseName = Database;
+
+ NSchemeCache::TSchemeCacheNavigate::TEntry entry;
+ entry.Operation = NSchemeCache::TSchemeCacheNavigate::OpPath;
+ entry.Path = SplitPath(TString(Database));
+ entry.RedirectRequired = false;
+
+ request->ResultSet.emplace_back(std::move(entry));
+
+ ctx.Send(MakeSchemeCacheID(), new TEvTxProxySchemeCache::TEvNavigateKeySet(request.release()));
+ Become(&TThis::StateNavigate);
+}
+
+NTabletPipe::TClientConfig TAuthActorBase::GetPipeClientConfig() const {
+ NTabletPipe::TClientConfig clientConfig;
+ clientConfig.RetryPolicy = {.RetryLimitCount = 3};
+ return clientConfig;
+}
+
+void TAuthActorBase::CleanupAndDie(const TActorContext &ctx) {
+ if (PipeClient) {
+ NTabletPipe::CloseClient(SelfId(), PipeClient);
+ }
+ return Die(ctx);
+}
+
+const TDuration TAuthActorBase::Timeout = TDuration::MilliSeconds(30000);
+
+TPlainAuthActorBase::TPlainAuthActorBase(TActorId sender, const std::string& database,
+ const std::string& authMsg, const std::string& peerName
+)
+ : TAuthActorBase(sender, database, peerName)
+ , AuthMsg(authMsg)
+{
+}
+
+void TPlainAuthActorBase::ProcessAuthMsg(const TActorContext &ctx) {
+ std::vector<std::string> authMsgParts = StringSplitter(AuthMsg).Split('\0');
+ if (authMsgParts.size() != 3) {
+ std::string error = "Wrong SASL PLAIN auth message format";
+ LOG_WARN_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, error);
+ return CleanupAndDie(ctx);
+ }
+
+ AuthzId = authMsgParts[0];
+ AuthcId = authMsgParts[1];
+ Passwd = authMsgParts[2];
+
+ if (!AuthzId.empty()) {
+ std::string prepAuthzId;
+ auto saslPrepRC = NLogin::NSasl::SaslPrep(AuthzId, prepAuthzId);
+ if (saslPrepRC != NLogin::NSasl::ESaslPrepReturnCodes::Success) {
+ std::string error = "Unsupported characters in the authorization identity";
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, error);
+ return CleanupAndDie(ctx);
+ }
+
+ AuthzId = std::move(prepAuthzId);
+ }
+
+ if (AuthcId.empty()) {
+ std::string error = "Empty authentication identity";
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, error);
+ return CleanupAndDie(ctx);
+ } else {
+ std::string prepAuthcId;
+ auto saslPrepRC = NLogin::NSasl::SaslPrep(AuthcId, prepAuthcId);
+ if (saslPrepRC != NLogin::NSasl::ESaslPrepReturnCodes::Success) {
+ std::string error = "Unsupported characters in the authentication identity";
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ DerivedActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, error);
+ return CleanupAndDie(ctx);
+ }
+
+ AuthcId = std::move(prepAuthcId);
+ }
+}
+
+}
diff --git a/ydb/core/security/sasl/base_auth_actors.h b/ydb/core/security/sasl/base_auth_actors.h
new file mode 100644
index 00000000000..af18d604c0a
--- /dev/null
+++ b/ydb/core/security/sasl/base_auth_actors.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include <ydb/core/tx/scheme_cache/scheme_cache.h>
+#include <ydb/core/tx/schemeshard/schemeshard.h>
+
+#include <ydb/library/actors/core/actor_bootstrapped.h>
+
+
+namespace NKikimr::NSasl {
+
+class TAuthActorBase : public TActorBootstrapped<TAuthActorBase> {
+public:
+ TAuthActorBase(TActorId sender, const std::string& database, const std::string& peerName);
+
+ STATEFN(StateNavigate) {
+ switch (ev->GetTypeRewrite()) {
+ HFunc(TEvTxProxySchemeCache::TEvNavigateKeySetResult, HandleNavigate);
+ }
+ }
+
+ STATEFN(StateLogin) {
+ switch (ev->GetTypeRewrite()) {
+ HFunc(TEvents::TEvUndelivered, HandleUndelivered);
+ HFunc(TEvTabletPipe::TEvClientConnected, HandleConnect);
+ HFunc(TEvTabletPipe::TEvClientDestroyed, HandleDestroyed);
+ HFunc(NSchemeShard::TEvSchemeShard::TEvLoginResult, HandleLoginResult);
+ CFunc(TEvents::TSystem::Wakeup, HandleTimeout);
+ }
+ }
+
+ virtual void Bootstrap(const TActorContext &ctx) = 0;
+
+ void HandleNavigate(TEvTxProxySchemeCache::TEvNavigateKeySetResult::TPtr& ev, const TActorContext &ctx);
+ void HandleUndelivered(TEvents::TEvUndelivered::TPtr&, const TActorContext &ctx);
+ void HandleDestroyed(TEvTabletPipe::TEvClientDestroyed::TPtr&, const TActorContext &ctx);
+ void HandleConnect(TEvTabletPipe::TEvClientConnected::TPtr& ev, const TActorContext &ctx);
+ void HandleLoginResult(NSchemeShard::TEvSchemeShard::TEvLoginResult::TPtr& ev, const TActorContext &ctx);
+ void HandleTimeout(const TActorContext &ctx);
+
+protected:
+ void ResolveSchemeShard(const TActorContext &ctx);
+ virtual NKikimrScheme::TEvLogin CreateLoginRequest() const = 0;
+ virtual void SendIssuedToken(const NKikimrScheme::TEvLoginResult& loginResult) const = 0;
+ virtual void SendError(NKikimrIssues::TIssuesIds::EIssueCode issueCode, const std::string& message,
+ const std::string& reason = "") const = 0;
+ void CleanupAndDie(const TActorContext &ctx);
+
+private:
+ NTabletPipe::TClientConfig GetPipeClientConfig() const;
+
+protected:
+ std::string_view DerivedActorName = "";
+
+ const TActorId Sender;
+ const std::string Database;
+ const std::string PeerName;
+
+ std::string AuthcId;
+
+private:
+ TActorId PipeClient;
+
+ static const TDuration Timeout;
+};
+
+class TPlainAuthActorBase : public TAuthActorBase {
+public:
+ TPlainAuthActorBase(TActorId sender, const std::string& database, const std::string& authMsg,
+ const std::string& peerName);
+
+protected:
+ void ProcessAuthMsg(const TActorContext &ctx);
+
+protected:
+ const std::string AuthMsg;
+
+ std::string AuthzId;
+ std::string Passwd;
+
+};
+
+}
diff --git a/ydb/core/security/sasl/events.h b/ydb/core/security/sasl/events.h
index 52c07d2e04d..9c32d41420c 100644
--- a/ydb/core/security/sasl/events.h
+++ b/ydb/core/security/sasl/events.h
@@ -2,12 +2,15 @@
#include <ydb/core/base/events.h>
#include <ydb/library/actors/core/events.h>
+#include <yql/essentials/public/issue/yql_issue.h>
namespace NKikimr::NSasl {
struct TEvSasl {
enum EEv {
EvComputedHashes = EventSpaceBegin(TKikimrEvents::ES_SASL_AUTH),
+ EvSaslPlainLoginResponse,
+ EvSaslPlainLdapLoginResponse,
EvEnd,
};
@@ -16,6 +19,21 @@ namespace NKikimr::NSasl {
std::string Hashes;
std::string ArgonHash;
};
+
+ struct TEvSaslPlainLoginResponse : public TEventLocal<TEvSaslPlainLoginResponse, EvSaslPlainLoginResponse> {
+ NYql::TIssue Issue;
+ std::string Token;
+ std::string SanitizedToken;
+ bool IsAdmin;
+ };
+
+ struct TEvSaslPlainLdapLoginResponse : public TEventLocal<TEvSaslPlainLdapLoginResponse, EvSaslPlainLdapLoginResponse> {
+ NYql::TIssue Issue;
+ std::string Reason;
+ std::string Token;
+ std::string SanitizedToken;
+ bool IsAdmin;
+ };
};
} // namespace NKikimr::NSasl
diff --git a/ydb/core/security/sasl/plain_auth_actor.cpp b/ydb/core/security/sasl/plain_auth_actor.cpp
new file mode 100644
index 00000000000..73a60626476
--- /dev/null
+++ b/ydb/core/security/sasl/plain_auth_actor.cpp
@@ -0,0 +1,261 @@
+#include "plain_auth_actor.h"
+
+#include <library/cpp/digest/argonish/argon2.h>
+#include <library/cpp/string_utils/base64/base64.h>
+
+#include <ydb/core/protos/auth.pb.h>
+#include <ydb/core/security/login_shared_func.h>
+#include <ydb/core/security/sasl/base_auth_actors.h>
+#include <ydb/core/security/sasl/events.h>
+#include <ydb/core/security/sasl/static_credentials_provider.h>
+
+#include <ydb/library/login/hashes_checker/hash_types.h>
+#include <ydb/library/login/hashes_checker/hashes_checker.h>
+#include <ydb/library/login/sasl/scram.h>
+
+
+using namespace NLoginProto;
+
+namespace {
+
+const std::vector<EHashType::HashType> ALLOWED_HASHES_TO_AUTH = {
+ // Ordered by priority
+ EHashType::ScramSha256,
+ EHashType::Argon,
+};
+
+bool IsBase64(const std::string& value) {
+ try {
+ Base64StrictDecode(value);
+ return true;
+ } catch (...) {
+ return false;
+ }
+}
+
+} // namespace
+
+namespace NKikimr::NSasl {
+
+using namespace NActors;
+using namespace NLogin;
+
+class TPlainAuthActor : public TPlainAuthActorBase {
+public:
+ TPlainAuthActor(TActorId sender, const std::string& database, const std::string& authMsg, const std::string& peerName)
+ : TPlainAuthActorBase(sender, database, authMsg, peerName)
+ {
+ DerivedActorName = ActorName;
+ }
+
+ virtual void Bootstrap(const TActorContext &ctx) override final {
+ if (!AppData(ctx)->AuthConfig.GetEnableLoginAuthentication()) {
+ std::string error = "Login authentication is disabled";
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, error);
+ return CleanupAndDie(ctx);
+ }
+
+ ProcessAuthMsg(ctx);
+
+ const auto [credsLookupResult, userHashInitParams] = TStaticCredentialsProvider::GetInstance()
+ .GetUserHashInitParams(Database, AuthcId);
+ CredsLookupResult = credsLookupResult;
+
+ // it can happen if SchemeShard works on a old version and doesn't pass hashes params
+ // after migration it has to become an error
+ if (CredsLookupResult == TStaticCredentialsProvider::UnknownDatabase) {
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "Unknown database or SchemeShard works on old version"
+ );
+ ResolveSchemeShard(ctx);
+ return;
+ } else if (CredsLookupResult == TStaticCredentialsProvider::UnknownUser) {
+ std::stringstream error;
+ error << "Cannot find user '" << AuthcId << "'";
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "Authentication failed: " << error.str();
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, error.str());
+ return CleanupAndDie(ctx);
+ }
+
+ ComputeHash(ctx, userHashInitParams);
+ ResolveSchemeShard(ctx);
+ return;
+ }
+
+private:
+ virtual NKikimrScheme::TEvLogin CreateLoginRequest() const override final {
+ if (CredsLookupResult == TStaticCredentialsProvider::UnknownDatabase) { // for backward compatibility
+ return NKikimr::CreatePlainLoginRequestOldFormat(TString(AuthcId), TString(Passwd), TString(PeerName),
+ AppData()->AuthConfig);
+ } else {
+ return NKikimr::CreatePlainLoginRequest(TString(AuthcId), ChosenAuthHashType, TString(ComputedHash),
+ TString(PeerName), AppData()->AuthConfig);
+ }
+ }
+
+ virtual void SendIssuedToken(const NKikimrScheme::TEvLoginResult& loginResult) const override final {
+ auto response = std::make_unique<TEvSasl::TEvSaslPlainLoginResponse>();
+ response->Issue = MakeIssue(NKikimrIssues::TIssuesIds::SUCCESS);
+ response->Token = loginResult.GetToken();
+ response->SanitizedToken = loginResult.GetSanitizedToken();
+ response->IsAdmin = loginResult.GetIsAdmin();
+
+ SendResponse(std::move(response));
+ }
+
+ virtual void SendError(NKikimrIssues::TIssuesIds::EIssueCode issueCode, const std::string& message,
+ [[maybe_unused]] const std::string& reason = "") const override final
+ {
+ auto response = std::make_unique<TEvSasl::TEvSaslPlainLoginResponse>();
+ response->Issue = MakeIssue(issueCode, TString(message));
+ SendResponse(std::move(response));
+ }
+
+ std::string ComputeArgonHash(const THashTypeDescription& hashParams, const std::string& salt) const {
+ std::string hash;
+ hash.resize(hashParams.HashSize);
+ ArgonHasher->Hash(
+ reinterpret_cast<const ui8*>(Passwd.data()),
+ Passwd.size(),
+ reinterpret_cast<const ui8*>(salt.data()),
+ salt.size(),
+ reinterpret_cast<ui8*>(hash.data()),
+ hash.size());
+
+ return Base64Encode(hash);
+ }
+
+ std::string ComputeScramHash(const THashTypeDescription& hashParams,
+ ui32 iterationsCount, const std::string& salt, std::string& error) const
+ {
+ std::string serverKey;
+ if (!NLogin::NSasl::ComputeServerKey(hashParams.Name, Passwd, salt, iterationsCount,
+ serverKey, error))
+ {
+ return "";
+ };
+
+ return Base64Encode(serverKey);
+ }
+
+ void ComputeHash(const TActorContext &ctx,
+ const std::unordered_map<NLoginProto::EHashType::HashType, std::string>& hashesInitParams)
+ {
+ std::string computedHash;
+ for (const auto& allowedHashType : ALLOWED_HASHES_TO_AUTH) {
+ const auto itHashesInitParams = hashesInitParams.find(allowedHashType);
+ if (itHashesInitParams == hashesInitParams.end()) {
+ continue;
+ }
+
+ ChosenAuthHashType = allowedHashType;
+ const auto& hashTypeDescr = HashesRegistry.HashTypesMap.at(ChosenAuthHashType);
+ switch (hashTypeDescr.Class) {
+ case EHashClass::Argon: {
+ if (!IsBase64(itHashesInitParams->second)) {
+ LOG_ERROR_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "Authentication failed: " <<
+ "'" << AuthcId << "' has broken Argon hash";
+ );
+ SendError(NKikimrIssues::TIssuesIds::UNEXPECTED, "");
+ return CleanupAndDie(ctx);
+ }
+
+ const auto argonSalt = Base64StrictDecode(itHashesInitParams->second);
+ auto argonHash = ComputeArgonHash(hashTypeDescr, argonSalt);
+ ComputedHash = std::move(argonHash);
+ break;
+ }
+ case EHashClass::Scram: {
+ if (Passwd.empty()) {
+ std::string error = "Empty password";
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, error);
+ return CleanupAndDie(ctx);
+ }
+
+ const auto scramInitParams = ParseScramHashInitParams(itHashesInitParams->second);
+ ui32 iterationsCount;
+ if (!TryFromString(scramInitParams.IterationsCount, iterationsCount)
+ || !IsBase64(scramInitParams.Salt)) {
+ LOG_ERROR_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "Authentication failed: " <<
+ "'" << AuthcId << "' has broken Scram hash";
+ );
+ SendError(NKikimrIssues::TIssuesIds::UNEXPECTED, "");
+ return CleanupAndDie(ctx);
+ }
+
+ const auto scramSalt = Base64StrictDecode(scramInitParams.Salt);
+
+ std::string error;
+ auto scramHash = ComputeScramHash(hashTypeDescr, iterationsCount, scramSalt, error);
+ if (!error.empty()) {
+ std::string error = "Unsupported characters in the password";
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << error
+ );
+ SendError(NKikimrIssues::TIssuesIds::ACCESS_DENIED, error);
+ return CleanupAndDie(ctx);
+ }
+
+ ComputedHash = std::move(scramHash);
+ break;
+ }
+ }
+
+ break;
+ }
+
+ if (ChosenAuthHashType == EHashType::Unknown) {
+ LOG_ERROR_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "Authentication failed: " <<
+ "'" << AuthcId << "' has no hashes";
+ );
+ SendError(NKikimrIssues::TIssuesIds::UNEXPECTED, "");
+ return CleanupAndDie(ctx);
+ }
+ }
+
+ void SendResponse(std::unique_ptr<TEvSasl::TEvSaslPlainLoginResponse> response) const {
+ Send(Sender, response.release());
+ }
+
+private:
+ TStaticCredentialsProvider::ELookupResultCode CredsLookupResult;
+ NLoginProto::EHashType::HashType ChosenAuthHashType = EHashType::Unknown;
+ std::string ComputedHash;
+
+ static const std::unique_ptr<const NArgonish::IArgon2Base> ArgonHasher;
+ static constexpr std::string_view ActorName = "TPlainAuthActor";
+};
+
+const std::unique_ptr<const NArgonish::IArgon2Base> TPlainAuthActor::ArgonHasher(Default<NArgonish::TArgon2Factory>().Create(
+ NArgonish::EArgon2Type::Argon2id, // Mixed version of Argon2
+ 2, // 2-pass computation
+ (1<<11), // 2 mebibytes memory usage (in KiB)
+ 1 // number of threads and lanes
+).Release());
+
+std::unique_ptr<IActor> CreatePlainAuthActor(
+ TActorId sender, const std::string& database, const std::string& saslPlainAuthMsg, const std::string& peerName)
+{
+ return std::make_unique<TPlainAuthActor>(sender, database, saslPlainAuthMsg, peerName);
+}
+
+} // namespace NKikimr::NSasl
diff --git a/ydb/core/security/sasl/plain_auth_actor.h b/ydb/core/security/sasl/plain_auth_actor.h
new file mode 100644
index 00000000000..2273f8b6dc4
--- /dev/null
+++ b/ydb/core/security/sasl/plain_auth_actor.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <ydb/library/actors/core/actor.h>
+
+namespace NKikimr::NSasl {
+
+std::unique_ptr<NActors::IActor> CreatePlainAuthActor(
+ NActors::TActorId sender, const std::string& database, const std::string& saslPlainAuthMsg, const std::string& peerName);
+
+} // namespace NKikimr::NSasl
diff --git a/ydb/core/security/sasl/plain_ldap_auth_proxy_actor.cpp b/ydb/core/security/sasl/plain_ldap_auth_proxy_actor.cpp
new file mode 100644
index 00000000000..2562f9713ac
--- /dev/null
+++ b/ydb/core/security/sasl/plain_ldap_auth_proxy_actor.cpp
@@ -0,0 +1,111 @@
+#include "plain_ldap_auth_proxy_actor.h"
+
+#include <ydb/core/security/ldap_auth_provider/ldap_auth_provider.h>
+#include <ydb/core/security/login_shared_func.h>
+#include <ydb/core/security/sasl/base_auth_actors.h>
+#include <ydb/core/security/sasl/events.h>
+
+using namespace NLoginProto;
+
+namespace NKikimr::NSasl {
+
+using namespace NActors;
+using namespace NLogin;
+using namespace NSchemeShard;
+
+class TPlainLdapAuthProxyActor : public TPlainAuthActorBase {
+public:
+ TPlainLdapAuthProxyActor(TActorId sender, const std::string& database, const std::string& authMsg, const std::string& peerName)
+ : TPlainAuthActorBase(sender, database, authMsg, peerName)
+ {
+ }
+
+ STATEFN(StateLdapAuthentication) {
+ switch (ev->GetTypeRewrite()) {
+ HFunc(TEvLdapAuthProvider::TEvAuthenticateResponse, Handle);
+ }
+ }
+
+ virtual void Bootstrap(const TActorContext &ctx) override final {
+ ProcessAuthMsg(ctx);
+
+ auto event = std::make_unique<TEvLdapAuthProvider::TEvAuthenticateRequest>(TString(AuthcId), TString(Passwd));
+ ctx.Send(MakeLdapAuthProviderID(), event.release());
+ Become(&TPlainLdapAuthProxyActor::StateLdapAuthentication);
+ return;
+ }
+
+ void Handle(TEvLdapAuthProvider::TEvAuthenticateResponse::TPtr& ev, const TActorContext &ctx) {
+ const TEvLdapAuthProvider::TEvAuthenticateResponse& response = *ev->Get();
+
+ LOG_DEBUG_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ " Handle TEvLdapAuthProvider::TEvAuthenticateResponse" <<
+ ", " << "status: " << static_cast<ui64>(response.Status) <<
+ ", " << "error: " << response.Error;
+ );
+
+ if (response.Status == TEvLdapAuthProvider::EStatus::SUCCESS) {
+ ResolveSchemeShard(ctx);
+ } else {
+ LOG_INFO_S(ctx, NKikimrServices::SASL_AUTH,
+ ActorName << "# " << ctx.SelfID.ToString() <<
+ ", " << "LDAP authentication failed: "<< response.Error
+ );
+ SendError(ConvertLdapStatus(response.Status), response.Error.Message, response.Error.LogMessage);
+ return CleanupAndDie(ctx);
+ }
+ }
+
+private:
+ virtual NKikimrScheme::TEvLogin CreateLoginRequest() const override final {
+ return CreatePlainLdapLoginRequest(TString(AuthcId), TString(PeerName), AppData()->AuthConfig);
+ }
+
+ virtual void SendIssuedToken(const NKikimrScheme::TEvLoginResult& loginResult) const override final {
+ auto response = std::make_unique<TEvSasl::TEvSaslPlainLdapLoginResponse>();
+ response->Issue = MakeIssue(NKikimrIssues::TIssuesIds::SUCCESS);
+ response->Token = loginResult.GetToken();
+ response->SanitizedToken = loginResult.GetSanitizedToken();
+ response->IsAdmin = loginResult.GetIsAdmin();
+
+ SendResponse(std::move(response));
+ }
+
+ virtual void SendError(NKikimrIssues::TIssuesIds::EIssueCode issueCode, const std::string& message,
+ const std::string& reason = "") const override final
+ {
+ auto response = std::make_unique<TEvSasl::TEvSaslPlainLdapLoginResponse>();
+ response->Issue = MakeIssue(issueCode, TString(message));
+ response->Reason = reason;
+ SendResponse(std::move(response));
+ }
+
+ static NKikimrIssues::TIssuesIds::EIssueCode ConvertLdapStatus(const TEvLdapAuthProvider::EStatus& status) {
+ switch (status) {
+ case NKikimr::TEvLdapAuthProvider::EStatus::SUCCESS:
+ return NKikimrIssues::TIssuesIds::SUCCESS;
+ case NKikimr::TEvLdapAuthProvider::EStatus::UNAUTHORIZED:
+ return NKikimrIssues::TIssuesIds::ACCESS_DENIED;
+ case NKikimr::TEvLdapAuthProvider::EStatus::UNAVAILABLE:
+ return NKikimrIssues::TIssuesIds::YDB_AUTH_UNAVAILABLE;
+ case NKikimr::TEvLdapAuthProvider::EStatus::BAD_REQUEST:
+ return NKikimrIssues::TIssuesIds::YDB_API_VALIDATION_ERROR;
+ }
+ }
+
+ void SendResponse(std::unique_ptr<TEvSasl::TEvSaslPlainLdapLoginResponse> response) const {
+ Send(Sender, response.release());
+ }
+
+private:
+ static constexpr std::string_view ActorName = "TPlainLdapAuthProxyActor";
+};
+
+std::unique_ptr<IActor> CreatePlainLdapAuthProxyActor(
+ TActorId sender, const std::string& database, const std::string& saslPlainAuthMsg, const std::string& peerName)
+{
+ return std::make_unique<TPlainLdapAuthProxyActor>(sender, database, saslPlainAuthMsg, peerName);
+}
+
+} // namespace NKikimr::NSasl
diff --git a/ydb/core/security/sasl/plain_ldap_auth_proxy_actor.h b/ydb/core/security/sasl/plain_ldap_auth_proxy_actor.h
new file mode 100644
index 00000000000..df463c5d3db
--- /dev/null
+++ b/ydb/core/security/sasl/plain_ldap_auth_proxy_actor.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <ydb/library/actors/core/actor.h>
+
+namespace NKikimr::NSasl {
+
+std::unique_ptr<NActors::IActor> CreatePlainLdapAuthProxyActor(
+ NActors::TActorId sender, const std::string& database, const std::string& saslPlainAuthMsg, const std::string& peerName);
+
+} // namespace NKikimr::NSasl
diff --git a/ydb/core/security/sasl/static_credentials_provider.cpp b/ydb/core/security/sasl/static_credentials_provider.cpp
new file mode 100644
index 00000000000..fff3100af99
--- /dev/null
+++ b/ydb/core/security/sasl/static_credentials_provider.cpp
@@ -0,0 +1,102 @@
+#include "static_credentials_provider.h"
+
+namespace NKikimr::NSasl {
+
+TStaticCredentialsProvider::TStaticCredentialsProvider()
+ : StaticCredsStorage(new std::unordered_map<std::string, TDatabaseUsersHashes>())
+{
+}
+
+TStaticCredentialsProvider& TStaticCredentialsProvider::GetInstance() {
+ static TStaticCredentialsProvider instance;
+ return instance;
+}
+
+std::pair<TStaticCredentialsProvider::ELookupResultCode, std::unordered_map<NLoginProto::EHashType::HashType, std::string>>
+TStaticCredentialsProvider::GetUserHashInitParams(const std::string& database, const std::string& username) const
+{
+ TTrueAtomicSharedPtr staticCredsStorage(StaticCredsStorage);
+ auto itDb = staticCredsStorage->find(database);
+ if (itDb == staticCredsStorage->end()) {
+ return {ELookupResultCode::UnknownDatabase, {}};
+ }
+
+ TTrueAtomicSharedPtr databaseUsersHashes(itDb->second);
+ auto itUser = databaseUsersHashes->find(username);
+ if (itUser == databaseUsersHashes->end()) {
+ return {ELookupResultCode::UnknownUser, {}};
+ }
+
+ return {ELookupResultCode::Success, itUser->second};
+}
+
+// Updates user credentials for a specific database.
+// Thread-safety is achieved through copy-on-write semantics using TTrueAtomicSharedPtr.
+// When copying TTrueAtomicSharedPtr, only reference counters are incremented, not the underlying data.
+// New TTrueAtomicSharedPtr objects are created to avoid corrupting memory that may be accessed
+// by other threads still holding references to the old data.
+void TStaticCredentialsProvider::UpdateDatabaseUsers(const NLoginProto::TSecurityState& securityState) {
+ const auto& database = securityState.GetAudience();
+ // Create a new map for updated user hashes
+ TTrueAtomicSharedPtr usersHashes(new std::unordered_map<std::string, THashInitParams>());
+ usersHashes->reserve(securityState.SidsSize());
+ for (const auto& sid : securityState.GetSids()) {
+ if (sid.GetType() == NLoginProto::ESidType::USER) {
+ THashInitParams userHashesInitParams;
+ userHashesInitParams.reserve(sid.HashesInitParamsSize());
+ for (const auto& hashInitParams : sid.GetHashesInitParams()) {
+ userHashesInitParams.emplace(hashInitParams.GetHashType(), hashInitParams.GetInitParams());
+ }
+
+ usersHashes->emplace(sid.GetName(), std::move(userHashesInitParams));
+ }
+ }
+
+ // Copy the shared pointer (only increments reference counter, doesn't copy data)
+ TTrueAtomicSharedPtr staticCredsStorage(StaticCredsStorage);
+ auto itDb = staticCredsStorage->find(database);
+ if (itDb == staticCredsStorage->end()) {
+ // Database doesn't exist - create a new storage map to avoid modifying the existing one
+ // that other threads might be reading. This ensures thread-safe copy-on-write behavior.
+ TTrueAtomicSharedPtr newStaticCredsStorage(new std::unordered_map<std::string, TDatabaseUsersHashes>);
+ // Copy all existing databases (only increments reference counters for nested shared pointers)
+ for (const auto& [databaseName, databaseUsersHashes] : *staticCredsStorage) {
+ newStaticCredsStorage->emplace(databaseName, databaseUsersHashes);
+ }
+ // Add the new database
+ newStaticCredsStorage->emplace(database, usersHashes);
+
+ // Atomically swap the storage pointer
+ StaticCredsStorage.swap(newStaticCredsStorage);
+ } else {
+ // Database exists - swap the user hashes for this database only
+ itDb->second.swap(usersHashes);
+ }
+}
+
+// Deletes all user credentials for a specific database.
+// Thread-safety is achieved through copy-on-write semantics using TTrueAtomicSharedPtr.
+// When copying TTrueAtomicSharedPtr, only reference counters are incremented, not the underlying data.
+// A new TTrueAtomicSharedPtr object is created to avoid corrupting memory that may be accessed
+// by other threads still holding references to the old data.
+void TStaticCredentialsProvider::DeleteDatabaseUsers(const std::string& database) {
+ // Copy the shared pointer (only increments reference counter, doesn't copy data)
+ TTrueAtomicSharedPtr staticCredsStorage(StaticCredsStorage);
+ auto itDb = staticCredsStorage->find(database);
+ if (itDb != staticCredsStorage->end()) {
+ // Database exists - create a new storage map to avoid modifying the existing one
+ // that other threads might be reading. This ensures thread-safe copy-on-write behavior.
+ TTrueAtomicSharedPtr newStaticCredsStorage(new std::unordered_map<std::string, TDatabaseUsersHashes>);
+ // Copy all databases except the one being deleted (only increments reference counters for nested shared pointers)
+ for (const auto& [databaseName, databaseUsersHashes] : *staticCredsStorage) {
+ if (databaseName != database) {
+ newStaticCredsStorage->emplace(databaseName, databaseUsersHashes);
+ }
+ }
+
+ // Atomically swap the storage pointer
+ StaticCredsStorage.swap(newStaticCredsStorage);
+ }
+}
+
+} // namespace NKikimr::NSasl
diff --git a/ydb/core/security/sasl/static_credentials_provider.h b/ydb/core/security/sasl/static_credentials_provider.h
new file mode 100644
index 00000000000..75333b5fc33
--- /dev/null
+++ b/ydb/core/security/sasl/static_credentials_provider.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <library/cpp/threading/atomic_shared_ptr/atomic_shared_ptr.h>
+
+#include <ydb/library/login/protos/login.pb.h>
+
+namespace NKikimr::NSasl {
+
+// TTrueAtomicSharedPtr provides thread-safe operations with different guarantees
+// depending on the platform architecture:
+//
+// x86_64 (amd64): Wait-free guarantee is provided for copying and destroying
+// shared pointers. The implementation uses 48 usable address bits and stores
+// reference counters in the high bits of 64-bit pointers.
+//
+// aarch64 (ARM64): Wait-free guarantee is NOT provided. The implementation uses
+// 52 usable address bits but relies on atomic operations that are only lock-free
+// (not wait-free) on this architecture. This requires more sophisticated algorithms
+// to achieve wait-free guarantees using lock-free atomic operations.
+//
+// When wait-free guarantees are not provided, update operations
+// for user credentials may experience slowdowns. This means that delays may be
+// observed during:
+// - User creation/deletion
+// - User credentials modification
+// - Database creation/deletion
+//
+// These delays occur because the underlying atomic operations may need to retry
+// in case of contention, rather than completing in a bounded number of steps.
+
+class TStaticCredentialsProvider {
+ using THashInitParams = std::unordered_map<NLoginProto::EHashType::HashType, std::string>;
+ using TDatabaseUsersHashes = TTrueAtomicSharedPtr<std::unordered_map<std::string, THashInitParams>>;
+
+public:
+ enum ELookupResultCode {
+ Success,
+ UnknownDatabase,
+ UnknownUser,
+ };
+
+public:
+ TStaticCredentialsProvider(const TStaticCredentialsProvider&) = delete;
+ TStaticCredentialsProvider(TStaticCredentialsProvider&&) = delete;
+ TStaticCredentialsProvider& operator=(const TStaticCredentialsProvider&) = delete;
+ TStaticCredentialsProvider& operator=(TStaticCredentialsProvider&&) = delete;
+
+ static TStaticCredentialsProvider& GetInstance();
+
+ std::pair<ELookupResultCode, THashInitParams> GetUserHashInitParams(const std::string& database,
+ const std::string& username) const;
+ void UpdateDatabaseUsers(const NLoginProto::TSecurityState& securityState);
+ void DeleteDatabaseUsers(const std::string& database);
+
+private:
+ TStaticCredentialsProvider();
+
+private:
+ TTrueAtomicSharedPtr<std::unordered_map<std::string, TDatabaseUsersHashes>> StaticCredsStorage;
+
+};
+
+} // namespace NKikimr::NSasl
diff --git a/ydb/core/security/sasl/ya.make b/ydb/core/security/sasl/ya.make
index 32273d6848c..c01c1abd1d0 100644
--- a/ydb/core/security/sasl/ya.make
+++ b/ydb/core/security/sasl/ya.make
@@ -6,14 +6,22 @@ PEERDIR(
library/cpp/json
library/cpp/string_utils/base64
ydb/core/protos
+ ydb/core/tx/scheme_cache
ydb/library/actors/core
ydb/library/login/hashes_checker
ydb/library/login/password_checker
+ ydb/library/login/protos
ydb/library/login/sasl
+ ydb/library/ydb_issue/proto
+ yql/essentials/public/issue
)
SRCS(
+ base_auth_actors.cpp
hasher.cpp
+ plain_auth_actor.cpp
+ plain_ldap_auth_proxy_actor.cpp
+ static_credentials_provider.cpp
)
END()
diff --git a/ydb/core/viewer/tests/canondata/result.json b/ydb/core/viewer/tests/canondata/result.json
index 79f7c3a5937..8aec4a83698 100644
--- a/ydb/core/viewer/tests/canondata/result.json
+++ b/ydb/core/viewer/tests/canondata/result.json
@@ -3529,10 +3529,12 @@
"PublicKeys": "not-empty-array",
"Sids": [
{
+ "HashesInitParams": "not-empty-array",
"Name": "monitoring",
"Type": 1
},
{
+ "HashesInitParams": "not-empty-array",
"Name": "viewer",
"Type": 1
},
@@ -3554,6 +3556,7 @@
"Type": 2
},
{
+ "HashesInitParams": "not-empty-array",
"Name": "database",
"Type": 1
},
@@ -3608,6 +3611,7 @@
"Type": 2
},
{
+ "HashesInitParams": "not-empty-array",
"Name": "root",
"Type": 1
}
diff --git a/ydb/core/viewer/tests/test.py b/ydb/core/viewer/tests/test.py
index ce221ec5818..57cd1299a81 100644
--- a/ydb/core/viewer/tests/test.py
+++ b/ydb/core/viewer/tests/test.py
@@ -537,6 +537,7 @@ class TestViewer(object):
'PathId',
'PublicKeys',
'OriginalUserToken',
+ 'HashesInitParams',
})
# groups
diff --git a/ydb/library/login/login.cpp b/ydb/library/login/login.cpp
index ec3198c1b94..29b8bc625fa 100644
--- a/ydb/library/login/login.cpp
+++ b/ydb/library/login/login.cpp
@@ -1063,7 +1063,13 @@ NLoginProto::TSecurityState TLoginProvider::GetSecurityState() const {
for (const auto& subSid : sidInfo.Members) {
sid.AddMembers(subSid);
}
- // no user hash here
+
+ // only init params of user hash
+ for (const auto& [hashType, hashRecord] : sidInfo.HashStorage) {
+ auto& hashParams = *sid.AddHashesInitParams();
+ hashParams.SetHashType(hashType);
+ hashParams.SetInitParams(hashRecord.HashInitParams);
+ }
}
}
return state;
diff --git a/ydb/library/login/protos/login.proto b/ydb/library/login/protos/login.proto
index 9d967f10401..927f623d768 100644
--- a/ydb/library/login/protos/login.proto
+++ b/ydb/library/login/protos/login.proto
@@ -33,6 +33,11 @@ message EHashType {
}
}
+message THashInitParams {
+ EHashType.HashType HashType = 1;
+ string InitParams = 2;
+}
+
message TSid {
string Name = 1;
ESidType.SidType Type = 2;
@@ -44,6 +49,7 @@ message TSid {
uint64 FailedLoginAttemptCount = 8;
bool IsEnabled = 9;
string PasswordHashes = 10; // Base64 encoded JSON map with hash type and hash secrets
+ repeated THashInitParams HashesInitParams = 11;
}
message TSecurityState {
diff --git a/ydb/library/login/sasl/scram.cpp b/ydb/library/login/sasl/scram.cpp
index 299da125216..0ad44af57a1 100644
--- a/ydb/library/login/sasl/scram.cpp
+++ b/ydb/library/login/sasl/scram.cpp
@@ -70,7 +70,7 @@ std::string Hi(const EVP_MD* md,
PRINT_HASH ("Hi() HMAC inside loop", tempResult, hashSize);
- for (k = 0; k < hashSize; k++) {
+ for (k = 0; k < hashSize; ++k) {
result[k] ^= tempResult[k];
}
@@ -154,4 +154,53 @@ bool GenerateScramSecrets(const std::string& hashType,
return true;
}
+bool ComputeServerKey(const std::string& hashType,
+ const std::string& password, const std::string& salt, ui32 iterationsCount,
+ std::string& serverKey, std::string& errorText)
+{
+ std::string digestName = GetDigestNameFromHashType(hashType);
+ const EVP_MD* md = EVP_get_digestbyname(digestName.c_str());
+ if (md == nullptr) {
+ errorText = "Unsupported hash type: " + hashType;
+ return false;
+ }
+
+ std::string prepPassword;
+ auto saslPrepRC = SaslPrep(password, prepPassword);
+ if (saslPrepRC != ESaslPrepReturnCodes::Success) {
+ errorText = "Unsupported password format";
+ return false;
+ }
+
+ // SaltedPassword := Hi(Normalize(password), salt, i)
+ std::string saltedPassword = Hi(md, prepPassword, salt, iterationsCount);
+
+ size_t hashSize = EVP_MD_size(md);
+ ui32 hashLen = 0;
+
+ serverKey.resize(hashSize);
+ // ServerKey := HMAC(SaltedPassword, "Server Key")
+ if (HMAC(md, saltedPassword.data(), saltedPassword.size(),
+ reinterpret_cast<const unsigned char*>(SERVER_KEY_CONSTANT.data()), SERVER_KEY_CONSTANT.size(),
+ reinterpret_cast<unsigned char*>(serverKey.data()), &hashLen) == nullptr)
+ {
+ errorText = "HMAC call failed";
+ return false;
+ }
+
+ return true;
+}
+
+std::string PrepareSaslPlainAuthMsg(const std::string& authenticationId, const std::string& password,
+ const std::string& authorizationId) {
+ std::string res;
+ res.reserve(authenticationId.size() + password.size() + authorizationId.size() + 2);
+ res += authorizationId;
+ res.push_back('\0');
+ res += authenticationId;
+ res.push_back('\0');
+ res += password;
+ return res;
+}
+
} // namespace NLogin::NSasl
diff --git a/ydb/library/login/sasl/scram.h b/ydb/library/login/sasl/scram.h
index 0a5a597a459..246f191c1c8 100644
--- a/ydb/library/login/sasl/scram.h
+++ b/ydb/library/login/sasl/scram.h
@@ -10,4 +10,11 @@ bool GenerateScramSecrets(const std::string& hashType,
const std::string& password, const std::string& salt, ui32 iterationsCount,
std::string& storedKey, std::string& serverKey, std::string& errorText);
+bool ComputeServerKey(const std::string& hashType,
+ const std::string& password, const std::string& salt, ui32 iterationsCount,
+ std::string& serverKey, std::string& errorText);
+
+std::string PrepareSaslPlainAuthMsg(const std::string& authenticationId, const std::string& password,
+ const std::string& authorizationId = "");
+
} // namespace NLogin::NSasl
diff --git a/ydb/services/ydb/ydb_common_ut.h b/ydb/services/ydb/ydb_common_ut.h
index 40b8ca233eb..e9287be5c32 100644
--- a/ydb/services/ydb/ydb_common_ut.h
+++ b/ydb/services/ydb/ydb_common_ut.h
@@ -137,6 +137,7 @@ public:
//Server_->GetRuntime()->SetLogPriority(NKikimrServices::TX_PROXY_SCHEME_CACHE, NActors::NLog::PRI_DEBUG);
//Server_->GetRuntime()->SetLogPriority(NKikimrServices::SCHEME_BOARD_REPLICA, NActors::NLog::PRI_DEBUG);
//Server_->GetRuntime()->SetLogPriority(NKikimrServices::FLAT_TX_SCHEMESHARD, NActors::NLog::PRI_INFO);
+ //Server_->GetRuntime()->SetLogPriority(NKikimrServices::SASL_AUTH, NActors::NLog::PRI_INFO);
//Server_->GetRuntime()->SetLogPriority(NKikimrServices::TX_PROXY, NActors::NLog::PRI_DEBUG);
//Server_->GetRuntime()->SetLogPriority(NKikimrServices::TX_OLAPSHARD, NActors::NLog::PRI_DEBUG);
//Server_->GetRuntime()->SetLogPriority(NKikimrServices::TX_COLUMNSHARD, NActors::NLog::PRI_DEBUG);