diff options
| author | Andrey Zaspa <[email protected]> | 2026-01-16 10:55:29 +0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-16 10:55:29 +0400 |
| commit | 60e4cc5f9cd154bf2e686ec96d015ad875d09386 (patch) | |
| tree | 24895c01f156ec2797888ff14933a35fa7cc4bb2 | |
| parent | ebc20a6541ab2bd9b6699a0e26a244b7dc40d4d2 (diff) | |
Compute password hashes on nodes processing Login requests (#31137)
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); |
