summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndrey Molotkov <[email protected]>2024-12-11 11:44:17 +0300
committerGitHub <[email protected]>2024-12-11 11:44:17 +0300
commite838a6204338f32ee77d077e0c4ed85a379d1d95 (patch)
tree793683b03972d48aa05c2184466e07932571ce4b
parent961df40e865111a37ebecd247c50ce316e36a402 (diff)
fstec: Add rules for password strength (#11963)
Co-authored-by: azevaykin <[email protected]>
-rw-r--r--ydb/core/cms/console/console__replace_yaml_config.cpp6
-rw-r--r--ydb/core/config/init/init_impl.h7
-rw-r--r--ydb/core/config/validation/auth_config_validator.cpp36
-rw-r--r--ydb/core/config/validation/auth_config_validator_ut/auth_config_validator_ut.cpp43
-rw-r--r--ydb/core/config/validation/auth_config_validator_ut/ya.make9
-rw-r--r--ydb/core/config/validation/validators.cpp14
-rw-r--r--ydb/core/config/validation/validators.h14
-rw-r--r--ydb/core/config/validation/ya.make3
-rw-r--r--ydb/core/protos/auth.proto11
-rw-r--r--ydb/core/tx/schemeshard/schemeshard_impl.cpp41
-rw-r--r--ydb/core/tx/schemeshard/schemeshard_impl.h4
-rw-r--r--ydb/core/tx/schemeshard/ut_login/ut_login.cpp126
-rw-r--r--ydb/library/login/login.cpp25
-rw-r--r--ydb/library/login/login.h6
-rw-r--r--ydb/library/login/login_ut.cpp66
-rw-r--r--ydb/library/login/password_checker/password_checker.cpp138
-rw-r--r--ydb/library/login/password_checker/password_checker.h77
-rw-r--r--ydb/library/login/password_checker/password_checker_ut.cpp171
-rw-r--r--ydb/library/login/password_checker/ut/ya.make9
-rw-r--r--ydb/library/login/password_checker/ya.make13
-rw-r--r--ydb/library/login/ya.make5
21 files changed, 823 insertions, 1 deletions
diff --git a/ydb/core/cms/console/console__replace_yaml_config.cpp b/ydb/core/cms/console/console__replace_yaml_config.cpp
index cbb063de7c4..905c921286c 100644
--- a/ydb/core/cms/console/console__replace_yaml_config.cpp
+++ b/ydb/core/cms/console/console__replace_yaml_config.cpp
@@ -3,6 +3,7 @@
#include "console_audit.h"
#include <ydb/core/tablet_flat/tablet_flat_executed.h>
+#include <ydb/core/config/validation/validators.h>
#include <ydb/library/aclib/aclib.h>
#include <ydb/library/yaml_config/yaml_config.h>
#include <yql/essentials/public/issue/protos/issue_severity.pb.h>
@@ -100,12 +101,17 @@ public:
UnknownFieldsCollector = new NYamlConfig::TBasicUnknownFieldsCollector;
+ std::vector<TString> errors;
for (auto& [_, config] : resolved.Configs) {
auto cfg = NYamlConfig::YamlToProto(
config.second,
true,
true,
UnknownFieldsCollector);
+ NKikimr::NConfig::EValidationResult result = NKikimr::NConfig::ValidateConfig(cfg, errors);
+ if (result == NKikimr::NConfig::EValidationResult::Error) {
+ ythrow yexception() << errors.front();
+ }
}
const auto& deprecatedPaths = NKikimrConfig::TAppConfig::GetReservedChildrenPaths();
diff --git a/ydb/core/config/init/init_impl.h b/ydb/core/config/init/init_impl.h
index 659f7ab95e1..558db1609d2 100644
--- a/ydb/core/config/init/init_impl.h
+++ b/ydb/core/config/init/init_impl.h
@@ -18,6 +18,7 @@
#include <ydb/core/protos/tenant_pool.pb.h>
#include <ydb/core/protos/compile_service_config.pb.h>
#include <ydb/core/protos/cms.pb.h>
+#include <ydb/core/config/validation/validators.h>
#include <ydb/library/aclib/aclib.h>
#include <ydb/library/actors/core/log_iface.h>
#include <ydb/library/yaml_config/yaml_config.h>
@@ -1126,6 +1127,12 @@ public:
TenantName = FillTenantPoolConfig(CommonAppOptions);
+ std::vector<TString> errors;
+ EValidationResult result = ValidateConfig(AppConfig, errors);
+ if (result == EValidationResult::Error) {
+ ythrow yexception() << errors.front();
+ }
+
Logger.Out() << "configured" << Endl;
FillData(CommonAppOptions);
diff --git a/ydb/core/config/validation/auth_config_validator.cpp b/ydb/core/config/validation/auth_config_validator.cpp
new file mode 100644
index 00000000000..7f558b37a66
--- /dev/null
+++ b/ydb/core/config/validation/auth_config_validator.cpp
@@ -0,0 +1,36 @@
+#include <ydb/core/protos/auth.pb.h>
+#include <vector>
+#include <util/generic/string.h>
+#include "validators.h"
+
+
+namespace NKikimr::NConfig {
+namespace {
+
+EValidationResult ValidatePasswordComplexity(const NKikimrProto::TPasswordComplexity& passwordComplexity, std::vector<TString>&msg) {
+ size_t minCountOfRequiredChars = passwordComplexity.GetMinLowerCaseCount() +
+ passwordComplexity.GetMinUpperCaseCount() +
+ passwordComplexity.GetMinNumbersCount() +
+ passwordComplexity.GetMinSpecialCharsCount();
+ if (passwordComplexity.GetMinLength() < minCountOfRequiredChars) {
+ msg = std::vector<TString>{"password_complexity: Min length of password cannot be less than "
+ "total min counts of lower case chars, upper case chars, numbers and special chars"};
+ return EValidationResult::Error;
+ }
+ return EValidationResult::Ok;
+}
+
+} // namespace
+
+EValidationResult ValidateAuthConfig(const NKikimrProto::TAuthConfig& authConfig, std::vector<TString>& msg) {
+ EValidationResult validatePasswordComplexityResult = ValidatePasswordComplexity(authConfig.GetPasswordComplexity(), msg);
+ if (validatePasswordComplexityResult == EValidationResult::Error) {
+ return EValidationResult::Error;
+ }
+ if (msg.size() > 0) {
+ return EValidationResult::Warn;
+ }
+ return EValidationResult::Ok;
+}
+
+} // NKikimr::NConfig
diff --git a/ydb/core/config/validation/auth_config_validator_ut/auth_config_validator_ut.cpp b/ydb/core/config/validation/auth_config_validator_ut/auth_config_validator_ut.cpp
new file mode 100644
index 00000000000..8b68f4027aa
--- /dev/null
+++ b/ydb/core/config/validation/auth_config_validator_ut/auth_config_validator_ut.cpp
@@ -0,0 +1,43 @@
+#include <library/cpp/testing/unittest/registar.h>
+#include <ydb/core/config/validation/validators.h>
+#include <ydb/core/protos/auth.pb.h>
+#include <vector>
+
+using namespace NKikimr::NConfig;
+
+Y_UNIT_TEST_SUITE(AuthConfigValidation) {
+ Y_UNIT_TEST(AcceptValidPasswordComplexity) {
+ NKikimrProto::TAuthConfig authConfig;
+ NKikimrProto::TPasswordComplexity* validPasswordComplexity = authConfig.MutablePasswordComplexity();
+
+ validPasswordComplexity->SetMinLength(8);
+ validPasswordComplexity->SetMinLowerCaseCount(2);
+ validPasswordComplexity->SetMinUpperCaseCount(2);
+ validPasswordComplexity->SetMinNumbersCount(2);
+ validPasswordComplexity->SetMinSpecialCharsCount(2);
+
+ std::vector<TString> error;
+ EValidationResult result = ValidateAuthConfig(authConfig, error);
+ UNIT_ASSERT_EQUAL(result, EValidationResult::Ok);
+ UNIT_ASSERT_C(error.empty(), "Should not be errors");
+ }
+
+ Y_UNIT_TEST(CannotAcceptInvalidPasswordComplexity) {
+ NKikimrProto::TAuthConfig authConfig;
+ NKikimrProto::TPasswordComplexity* invalidPasswordComplexity = authConfig.MutablePasswordComplexity();
+
+ // 8 < 2 + 2 + 2 + 3
+ invalidPasswordComplexity->SetMinLength(8);
+ invalidPasswordComplexity->SetMinLowerCaseCount(2);
+ invalidPasswordComplexity->SetMinUpperCaseCount(2);
+ invalidPasswordComplexity->SetMinNumbersCount(2);
+ invalidPasswordComplexity->SetMinSpecialCharsCount(3);
+
+ std::vector<TString> error;
+ EValidationResult result = ValidateAuthConfig(authConfig, error);
+ UNIT_ASSERT_EQUAL(result, EValidationResult::Error);
+ UNIT_ASSERT_VALUES_EQUAL(error.size(), 1);
+ UNIT_ASSERT_STRINGS_EQUAL(error.front(), "password_complexity: Min length of password cannot be less than "
+ "total min counts of lower case chars, upper case chars, numbers and special chars");
+ }
+}
diff --git a/ydb/core/config/validation/auth_config_validator_ut/ya.make b/ydb/core/config/validation/auth_config_validator_ut/ya.make
new file mode 100644
index 00000000000..beeb68c81fa
--- /dev/null
+++ b/ydb/core/config/validation/auth_config_validator_ut/ya.make
@@ -0,0 +1,9 @@
+UNITTEST_FOR(ydb/core/config/validation)
+
+SRC(
+ auth_config_validator_ut.cpp
+)
+
+YQL_LAST_ABI_VERSION()
+
+END()
diff --git a/ydb/core/config/validation/validators.cpp b/ydb/core/config/validation/validators.cpp
index 36391d0e9ea..7b0deb510fa 100644
--- a/ydb/core/config/validation/validators.cpp
+++ b/ydb/core/config/validation/validators.cpp
@@ -161,4 +161,18 @@ EValidationResult ValidateStaticGroup(const NKikimrConfig::TAppConfig& current,
return EValidationResult::Ok;
}
+EValidationResult ValidateConfig(const NKikimrConfig::TAppConfig& config, std::vector<TString>& msg) {
+ if (config.HasAuthConfig()) {
+ NKikimr::NConfig::EValidationResult result = NKikimr::NConfig::ValidateAuthConfig(config.GetAuthConfig(), msg);
+ if (result == NKikimr::NConfig::EValidationResult::Error) {
+ return EValidationResult::Error;
+ }
+ }
+ if (msg.size() > 0) {
+ return EValidationResult::Warn;
+ }
+
+ return EValidationResult::Ok;
+}
+
} // namespace NKikimr::NConfig
diff --git a/ydb/core/config/validation/validators.h b/ydb/core/config/validation/validators.h
index 3e96c2c4afc..b09297cda3e 100644
--- a/ydb/core/config/validation/validators.h
+++ b/ydb/core/config/validation/validators.h
@@ -4,6 +4,12 @@
#include <vector>
+namespace NKikimrProto {
+
+class TAuthConfig;
+
+} // NKikimrProto
+
namespace NKikimr::NConfig {
enum class EValidationResult {
@@ -32,4 +38,12 @@ EValidationResult ValidateStaticGroup(
const NKikimrConfig::TAppConfig& proposed,
std::vector<TString>& msg);
+EValidationResult ValidateAuthConfig(
+ const NKikimrProto::TAuthConfig& authConfig,
+ std::vector<TString>& msg);
+
+EValidationResult ValidateConfig(
+ const NKikimrConfig::TAppConfig& config,
+ std::vector<TString>& msg);
+
} // namespace NKikimr::NConfig
diff --git a/ydb/core/config/validation/ya.make b/ydb/core/config/validation/ya.make
index db6f9c1a70f..47b4d18b9b3 100644
--- a/ydb/core/config/validation/ya.make
+++ b/ydb/core/config/validation/ya.make
@@ -3,6 +3,7 @@ LIBRARY()
SRCS(
validators.h
validators.cpp
+ auth_config_validator.cpp
)
PEERDIR(
@@ -13,5 +14,5 @@ END()
RECURSE_FOR_TESTS(
ut
+ auth_config_validator_ut
)
-
diff --git a/ydb/core/protos/auth.proto b/ydb/core/protos/auth.proto
index 6f25ed33952..e01eeda6bbb 100644
--- a/ydb/core/protos/auth.proto
+++ b/ydb/core/protos/auth.proto
@@ -56,6 +56,7 @@ message TAuthConfig {
optional string CertificateAuthenticationDomain = 80 [default = "cert"];
optional bool EnableLoginAuthentication = 81 [default = true];
optional string NodeRegistrationToken = 82 [default = "root@builtin", (Ydb.sensitive) = true];
+ optional TPasswordComplexity PasswordComplexity = 83;
}
message TUserRegistryConfig {
@@ -122,3 +123,13 @@ message TLdapAuthentication {
optional string Scheme = 11 [default = "ldap"];
optional TExtendedSettings ExtendedSettings = 12;
}
+
+message TPasswordComplexity {
+ optional uint32 MinLength = 1;
+ optional uint32 MinLowerCaseCount = 2;
+ optional uint32 MinUpperCaseCount = 3;
+ optional uint32 MinNumbersCount = 4;
+ optional uint32 MinSpecialCharsCount = 5;
+ optional string SpecialChars = 6;
+ optional bool CanContainUsername = 7;
+}
diff --git a/ydb/core/tx/schemeshard/schemeshard_impl.cpp b/ydb/core/tx/schemeshard/schemeshard_impl.cpp
index a706024ce48..fd52a583bf3 100644
--- a/ydb/core/tx/schemeshard/schemeshard_impl.cpp
+++ b/ydb/core/tx/schemeshard/schemeshard_impl.cpp
@@ -12,6 +12,7 @@
#include <ydb/core/base/tx_processing.h>
#include <ydb/core/protos/feature_flags.pb.h>
#include <ydb/core/protos/table_stats.pb.h> // for TStoragePoolsStats
+#include <ydb/core/protos/auth.pb.h>
#include <ydb/core/engine/mkql_proto.h>
#include <ydb/core/sys_view/partition_stats/partition_stats.h>
#include <ydb/core/statistics/events.h>
@@ -19,6 +20,7 @@
#include <ydb/core/scheme/scheme_types_proto.h>
#include <ydb/core/tx/columnshard/bg_tasks/events/events.h>
#include <ydb/core/tx/scheme_board/events_schemeshard.h>
+#include <ydb/library/login/password_checker/password_checker.h>
#include <yql/essentials/minikql/mkql_type_ops.h>
#include <yql/essentials/providers/common/proto/gateways_config.pb.h>
#include <util/random/random.h>
@@ -4440,6 +4442,15 @@ TSchemeShard::TSchemeShard(const TActorId &tablet, TTabletStorageInfo *info)
COUNTER_PQ_STATS_WRITTEN,
COUNTER_PQ_STATS_BATCH_LATENCY)
, AllowDataColumnForIndexTable(0, 0, 1)
+ , LoginProvider(NLogin::TPasswordComplexity({
+ .MinLength = AppData()->AuthConfig.GetPasswordComplexity().GetMinLength(),
+ .MinLowerCaseCount = AppData()->AuthConfig.GetPasswordComplexity().GetMinLowerCaseCount(),
+ .MinUpperCaseCount = AppData()->AuthConfig.GetPasswordComplexity().GetMinUpperCaseCount(),
+ .MinNumbersCount = AppData()->AuthConfig.GetPasswordComplexity().GetMinNumbersCount(),
+ .MinSpecialCharsCount = AppData()->AuthConfig.GetPasswordComplexity().GetMinSpecialCharsCount(),
+ .SpecialChars = AppData()->AuthConfig.GetPasswordComplexity().GetSpecialChars(),
+ .CanContainUsername = AppData()->AuthConfig.GetPasswordComplexity().GetCanContainUsername()
+ }))
{
TabletCountersPtr.Reset(new TProtobufTabletCounters<
ESimpleCounters_descriptor,
@@ -7128,6 +7139,10 @@ void TSchemeShard::ApplyConsoleConfigs(const NKikimrConfig::TAppConfig& appConfi
);
}
+ if (appConfig.HasAuthConfig()) {
+ ConfigureLoginProvider(appConfig.GetAuthConfig(), ctx);
+ }
+
if (IsSchemeShardConfigured()) {
StartStopCompactionQueues();
if (BackgroundCleaningQueue) {
@@ -7321,6 +7336,32 @@ void TSchemeShard::ConfigureBackgroundCleaningQueue(
<< ", InflightLimit# " << cleaningConfig.InflightLimit);
}
+void TSchemeShard::ConfigureLoginProvider(
+ const ::NKikimrProto::TAuthConfig& config,
+ const TActorContext &ctx)
+{
+ const auto& passwordComplexityConfig = config.GetPasswordComplexity();
+ NLogin::TPasswordComplexity passwordComplexity({
+ .MinLength = passwordComplexityConfig.GetMinLength(),
+ .MinLowerCaseCount = passwordComplexityConfig.GetMinLowerCaseCount(),
+ .MinUpperCaseCount = passwordComplexityConfig.GetMinUpperCaseCount(),
+ .MinNumbersCount = passwordComplexityConfig.GetMinNumbersCount(),
+ .MinSpecialCharsCount = passwordComplexityConfig.GetMinSpecialCharsCount(),
+ .SpecialChars = (passwordComplexityConfig.GetSpecialChars().empty() ? NLogin::TPasswordComplexity::VALID_SPECIAL_CHARS : passwordComplexityConfig.GetSpecialChars()),
+ .CanContainUsername = passwordComplexityConfig.GetCanContainUsername()
+ });
+ LoginProvider.UpdatePasswordCheckParameters(passwordComplexity);
+
+ LOG_NOTICE_S(ctx, NKikimrServices::FLAT_TX_SCHEMESHARD,
+ "PasswordComplexity for LoginProvider configured: MinLength# " << passwordComplexity.MinLength
+ << ", MinLowerCaseCount# " << passwordComplexity.MinLowerCaseCount
+ << ", MinUpperCaseCount# " << passwordComplexity.MinUpperCaseCount
+ << ", MinNumbersCount# " << passwordComplexity.MinNumbersCount
+ << ", MinSpecialCharsCount# " << passwordComplexity.MinSpecialCharsCount
+ << ", SpecialChars# " << (passwordComplexityConfig.GetSpecialChars().empty() ? NLogin::TPasswordComplexity::VALID_SPECIAL_CHARS : passwordComplexityConfig.GetSpecialChars())
+ << ", CanContainUsername# " << (passwordComplexity.CanContainUsername ? "true" : "false"));
+}
+
void TSchemeShard::StartStopCompactionQueues() {
// note, that we don't need to check current state of compaction queue
if (IsServerlessDomain(TPath::Init(RootPathId(), this))) {
diff --git a/ydb/core/tx/schemeshard/schemeshard_impl.h b/ydb/core/tx/schemeshard/schemeshard_impl.h
index 4f39324d948..e7152e984d0 100644
--- a/ydb/core/tx/schemeshard/schemeshard_impl.h
+++ b/ydb/core/tx/schemeshard/schemeshard_impl.h
@@ -497,6 +497,10 @@ public:
const NKikimrConfig::TBackgroundCleaningConfig& config,
const TActorContext &ctx);
+ void ConfigureLoginProvider(
+ const ::NKikimrProto::TAuthConfig& config,
+ const TActorContext &ctx);
+
void StartStopCompactionQueues();
void WaitForTableProfiles(ui64 importId, ui32 itemIdx);
diff --git a/ydb/core/tx/schemeshard/ut_login/ut_login.cpp b/ydb/core/tx/schemeshard/ut_login/ut_login.cpp
index d75cf0f49dd..48ffbb97660 100644
--- a/ydb/core/tx/schemeshard/ut_login/ut_login.cpp
+++ b/ydb/core/tx/schemeshard/ut_login/ut_login.cpp
@@ -1,6 +1,7 @@
#include <util/string/join.h>
#include <ydb/library/login/login.h>
+#include <ydb/library/login/password_checker/password_checker.h>
#include <ydb/library/actors/http/http_proxy.h>
#include <ydb/library/testlib/service_mocks/ldap_mock/ldap_simple_server.h>
#include <ydb/core/tx/schemeshard/ut_helpers/helpers.h>
@@ -14,6 +15,25 @@ using namespace NKikimr;
using namespace NSchemeShard;
using namespace NSchemeShardUT_Private;
+namespace NSchemeShardUT_Private {
+
+void SetPasswordCheckerParameters(TTestActorRuntime &runtime, ui64 schemeShard, const NLogin::TPasswordComplexity::TInitializer& initializer) {
+ auto request = MakeHolder<NConsole::TEvConsole::TEvConfigNotificationRequest>();
+
+ ::NKikimrProto::TPasswordComplexity passwordComplexity;
+ passwordComplexity.SetMinLength(initializer.MinLength);
+ passwordComplexity.SetMinLowerCaseCount(initializer.MinLowerCaseCount);
+ passwordComplexity.SetMinUpperCaseCount(initializer.MinUpperCaseCount);
+ passwordComplexity.SetMinNumbersCount(initializer.MinNumbersCount);
+ passwordComplexity.SetMinSpecialCharsCount(initializer.MinSpecialCharsCount);
+ passwordComplexity.SetSpecialChars(initializer.SpecialChars);
+ passwordComplexity.SetCanContainUsername(initializer.CanContainUsername);
+ *request->Record.MutableConfig()->MutableAuthConfig()->MutablePasswordComplexity() = passwordComplexity;
+ SetConfig(runtime, schemeShard, std::move(request));
+}
+
+} // namespace NSchemeShardUT_Private
+
Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) {
Y_UNIT_TEST(BasicLogin) {
@@ -51,6 +71,112 @@ Y_UNIT_TEST_SUITE(TSchemeShardLoginTest) {
UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().HasSecurityState());
UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize() > 0);
}
+
+ Y_UNIT_TEST(ChangeAcceptablePasswordParameters) {
+ TTestBasicRuntime runtime;
+ TTestEnv env(runtime);
+ ui64 txId = 100;
+ // Password parameters:
+ // min length 0
+ // optional: lower case, upper case, numbers, special symbols from list !@#$%^&*()_+{}|<>?=
+ // required: cannot contain username
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user1", "password1");
+ auto resultLogin = Login(runtime, "user1", "password1");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+ auto describe = DescribePath(runtime, TTestTxConfig::SchemeShard, "/MyRoot");
+ UNIT_ASSERT(describe.HasPathDescription());
+ UNIT_ASSERT(describe.GetPathDescription().HasDomainDescription());
+ UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().HasSecurityState());
+ UNIT_ASSERT(describe.GetPathDescription().GetDomainDescription().GetSecurityState().PublicKeysSize() > 0);
+
+ // Accept password without lower case symbols
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user2", "PASSWORDU2");
+ resultLogin = Login(runtime, "user2", "PASSWORDU2");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+ // Password parameters:
+ // min length 0
+ // optional: upper case, numbers, special symbols from list !@#$%^&*()_+{}|<>?=
+ // required: lower case = 3, cannot contain username
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLowerCaseCount = 3});
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user3", "PASSWORDU3", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 lower case character"}});
+ // Add lower case symbols to password
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user3", "PASswORDu3");
+ resultLogin = Login(runtime, "user3", "PASswORDu3");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+
+ // Accept password without upper case symbols
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user4", "passwordu4");
+ resultLogin = Login(runtime, "user4", "passwordu4");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+ // Password parameters:
+ // min length 0
+ // optional: lower case, numbers, special symbols from list !@#$%^&*()_+{}|<>?=
+ // required: upper case = 3, cannot contain username
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLowerCaseCount = 0, .MinUpperCaseCount = 3});
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user5", "passwordu5", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 upper case character"}});
+ // Add 3 upper case symbols to password
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user5", "PASswORDu5");
+ resultLogin = Login(runtime, "user5", "PASswORDu5");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+
+ // Accept short password
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinUpperCaseCount = 0});
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user6", "passwu6");
+ resultLogin = Login(runtime, "user6", "passwu6");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+ // Password parameters:
+ // min length 8
+ // optional: lower case, upper case, numbers, special symbols from list !@#$%^&*()_+{}|<>?=
+ // required: cannot contain username
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLength = 8});
+ // Too short password
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user7", "passwu7", {{NKikimrScheme::StatusPreconditionFailed, "Password is too short"}});
+ // Password has correct length
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user7", "passwordu7");
+ resultLogin = Login(runtime, "user7", "passwordu7");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+
+ // Accept password without numbers
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinLength = 0});
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user8", "passWorDueitgh");
+ resultLogin = Login(runtime, "user8", "passWorDueitgh");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+ // Password parameters:
+ // min length 0
+ // optional: lower case, upper case,special symbols from list !@#$%^&*()_+{}|<>?=
+ // required: numbers = 3, cannot contain username
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinNumbersCount = 3});
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user9", "passwordunine", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 number"}});
+ // Password with numbers
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user9", "pas1swo5rdu9");
+ resultLogin = Login(runtime, "user9", "pas1swo5rdu9");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+
+ // Accept password without special symbols
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinNumbersCount = 0});
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user10", "passWorDu10");
+ resultLogin = Login(runtime, "user10", "passWorDu10");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+ // Password parameters:
+ // min length 0
+ // optional: lower case, upper case, numbers
+ // required: special symbols from list !@#$%^&*()_+{}|<>?= , cannot contain username
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.MinSpecialCharsCount = 3});
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user11", "passwordu11", {{NKikimrScheme::StatusPreconditionFailed, "Incorrect password format: should contain at least 3 special character"}});
+ // Password with special symbols
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user11", "passwordu11*&%#");
+ resultLogin = Login(runtime, "user11", "passwordu11*&%#");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+ // Password parameters:
+ // min length 0
+ // optional: lower case, upper case, numbers
+ // required: special symbols from list *# , cannot contain username
+ SetPasswordCheckerParameters(runtime, TTestTxConfig::SchemeShard, {.SpecialChars = "*#"}); // Only 2 special symbols are valid
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user12", "passwordu12*&%#", {{NKikimrScheme::StatusPreconditionFailed, "Password contains unacceptable characters"}});
+ CreateAlterLoginCreateUser(runtime, ++txId, "/MyRoot", "user12", "passwordu12*#");
+ resultLogin = Login(runtime, "user12", "passwordu12*#");
+ UNIT_ASSERT_VALUES_EQUAL(resultLogin.error(), "");
+ }
}
namespace NSchemeShardUT_Private {
diff --git a/ydb/library/login/login.cpp b/ydb/library/login/login.cpp
index 62a56053242..3064f2dbcca 100644
--- a/ydb/library/login/login.cpp
+++ b/ydb/library/login/login.cpp
@@ -12,6 +12,8 @@
#include <deque>
+#include <ydb/library/login/password_checker/password_checker.h>
+
#include "login.h"
namespace NLogin {
@@ -35,6 +37,12 @@ struct TLoginProvider::TImpl {
TLoginProvider::TLoginProvider()
: Impl(new TImpl())
+ , PasswordChecker(TPasswordComplexity())
+{}
+
+TLoginProvider::TLoginProvider(const TPasswordComplexity& passwordComplexity)
+ : Impl(new TImpl())
+ , PasswordChecker(passwordComplexity)
{}
TLoginProvider::~TLoginProvider()
@@ -51,6 +59,13 @@ TLoginProvider::TBasicResponse TLoginProvider::CreateUser(const TCreateUserReque
response.Error = "Name is not allowed";
return response;
}
+
+ TPasswordChecker::TResult passwordCheckResult = PasswordChecker.Check(request.User, request.Password);
+ if (!passwordCheckResult.Success) {
+ response.Error = passwordCheckResult.Error;
+ return response;
+ }
+
auto itUserCreate = Sids.emplace(request.User, TSidRecord{.Type = NLoginProto::ESidType::USER});
if (!itUserCreate.second) {
if (itUserCreate.first->second.Type == ESidType::USER) {
@@ -86,6 +101,12 @@ TLoginProvider::TBasicResponse TLoginProvider::ModifyUser(const TModifyUserReque
return response;
}
+ TPasswordChecker::TResult passwordCheckResult = PasswordChecker.Check(request.User, request.Password);
+ if (!passwordCheckResult.Success) {
+ response.Error = passwordCheckResult.Error;
+ return response;
+ }
+
TSidRecord& user = itUserModify->second;
user.Hash = Impl->GenerateHash(request.Password);
@@ -659,4 +680,8 @@ TString TLoginProvider::SanitizeJwtToken(const TString& token) {
return TStringBuilder() << TStringBuf(token).SubString(0, signaturePos) << ".**"; // <token>.**
}
+void TLoginProvider::UpdatePasswordCheckParameters(const TPasswordComplexity& passwordComplexity) {
+ PasswordChecker.Update(passwordComplexity);
+}
+
}
diff --git a/ydb/library/login/login.h b/ydb/library/login/login.h
index a182bf9b6e6..83f005f676d 100644
--- a/ydb/library/login/login.h
+++ b/ydb/library/login/login.h
@@ -7,6 +7,7 @@
#include <deque>
#include <util/generic/string.h>
#include <ydb/library/login/protos/login.pb.h>
+#include <ydb/library/login/password_checker/password_checker.h>
namespace NLogin {
@@ -173,7 +174,10 @@ public:
TRenameGroupResponse RenameGroup(const TRenameGroupRequest& request);
TRemoveGroupResponse RemoveGroup(const TRemoveGroupRequest& request);
+ void UpdatePasswordCheckParameters(const TPasswordComplexity& passwordComplexity);
+
TLoginProvider();
+ TLoginProvider(const TPasswordComplexity& passwordComplexity);
~TLoginProvider();
std::vector<TString> GetGroupsMembership(const TString& member);
@@ -188,6 +192,8 @@ private:
struct TImpl;
THolder<TImpl> Impl;
+
+ TPasswordChecker PasswordChecker;
};
}
diff --git a/ydb/library/login/login_ut.cpp b/ydb/library/login/login_ut.cpp
index 9a63ca2eacc..248a519f7ed 100644
--- a/ydb/library/login/login_ut.cpp
+++ b/ydb/library/login/login_ut.cpp
@@ -1,5 +1,6 @@
#include <library/cpp/testing/unittest/registar.h>
#include <util/generic/algorithm.h>
+#include <ydb/library/login/password_checker/password_checker.h>
#include "login.h"
using namespace NLogin;
@@ -79,6 +80,71 @@ Y_UNIT_TEST_SUITE(Login) {
UNIT_ASSERT_VALUES_EQUAL(response3.Error, "Wrong audience");
}
+ Y_UNIT_TEST(TestModifyUser) {
+ TLoginProvider provider;
+ provider.Audience = "test_audience1";
+ provider.RotateKeys();
+ TLoginProvider::TCreateUserRequest createUser1Request {
+ .User = "user1",
+ .Password = "password1"
+ };
+ auto createUser1Response = provider.CreateUser(createUser1Request);
+ UNIT_ASSERT(!createUser1Response.Error);
+ TLoginProvider::TLoginUserRequest loginUser1Request1 {
+ .User = createUser1Request.User,
+ .Password = createUser1Request.Password
+ };
+ auto loginUser1Response1 = provider.LoginUser(loginUser1Request1);
+ UNIT_ASSERT_VALUES_EQUAL(loginUser1Response1.Error, "");
+ TLoginProvider::TValidateTokenRequest validateUser1TokenRequest1 {
+ .Token = loginUser1Response1.Token
+ };
+ auto validateUser1TokenResponse1 = provider.ValidateToken(validateUser1TokenRequest1);
+ UNIT_ASSERT_VALUES_EQUAL(validateUser1TokenResponse1.Error, "");
+ UNIT_ASSERT(validateUser1TokenResponse1.User == createUser1Request.User);
+
+ TPasswordComplexity passwordComplexity({
+ .MinLength = 8,
+ .MinLowerCaseCount = 2,
+ .MinUpperCaseCount = 2,
+ .MinNumbersCount = 2,
+ .MinSpecialCharsCount = 2,
+ .SpecialChars = TPasswordComplexity::VALID_SPECIAL_CHARS
+ });
+
+ provider.UpdatePasswordCheckParameters(passwordComplexity);
+
+ TLoginProvider::TModifyUserRequest modifyUser1RequestBad {
+ .User = createUser1Request.User,
+ .Password = "UserPassword1"
+ };
+
+ TLoginProvider::TBasicResponse modifyUser1ResponseBad = provider.ModifyUser(modifyUser1RequestBad);
+ UNIT_ASSERT(!modifyUser1ResponseBad.Error.empty());
+ UNIT_ASSERT_STRINGS_EQUAL(modifyUser1ResponseBad.Error, "Incorrect password format: should contain at least 2 number, should contain at least 2 special character");
+
+ TLoginProvider::TModifyUserRequest modifyUser1Request {
+ .User = createUser1Request.User,
+ .Password = "paS*sw1oR#d7"
+ };
+
+ TLoginProvider::TBasicResponse modifyUser1Response = provider.ModifyUser(modifyUser1Request);
+ UNIT_ASSERT_VALUES_EQUAL(modifyUser1Response.Error, "");
+
+ TLoginProvider::TLoginUserRequest loginUser1Request2 = {
+ .User = modifyUser1Request.User,
+ .Password = modifyUser1Request.Password
+ };
+ TLoginProvider::TLoginUserResponse loginUser1Response2 = provider.LoginUser(loginUser1Request2);
+ UNIT_ASSERT_VALUES_EQUAL(loginUser1Response2.Error, "");
+ TLoginProvider::TValidateTokenRequest validateUser1TokenRequest2 = {
+ .Token = loginUser1Response2.Token
+ };
+ TLoginProvider::TValidateTokenResponse validateUser1TokenResponse2 = provider.ValidateToken(validateUser1TokenRequest2);
+ UNIT_ASSERT_VALUES_EQUAL(validateUser1TokenResponse2.Error, "");
+ UNIT_ASSERT(validateUser1TokenResponse2.User == createUser1Request.User);
+ }
+
Y_UNIT_TEST(TestGroups) {
TLoginProvider provider;
provider.Audience = "test_audience1";
diff --git a/ydb/library/login/password_checker/password_checker.cpp b/ydb/library/login/password_checker/password_checker.cpp
new file mode 100644
index 00000000000..e39bb812a76
--- /dev/null
+++ b/ydb/library/login/password_checker/password_checker.cpp
@@ -0,0 +1,138 @@
+#include <cctype>
+#include <util/string/builder.h>
+#include "password_checker.h"
+
+namespace NLogin {
+
+TPasswordComplexity::TPasswordComplexity()
+ : SpecialChars(VALID_SPECIAL_CHARS.cbegin(), VALID_SPECIAL_CHARS.cend())
+{}
+
+TPasswordComplexity::TPasswordComplexity(const TInitializer& initializer)
+ : MinLength(initializer.MinLength)
+ , MinLowerCaseCount(initializer.MinLowerCaseCount)
+ , MinUpperCaseCount(initializer.MinUpperCaseCount)
+ , MinNumbersCount(initializer.MinNumbersCount)
+ , MinSpecialCharsCount(initializer.MinSpecialCharsCount)
+ , CanContainUsername(initializer.CanContainUsername)
+{
+ static const std::unordered_set<char> validSpecialChars(VALID_SPECIAL_CHARS.cbegin(), VALID_SPECIAL_CHARS.cend());
+ for (const char ch : initializer.SpecialChars) {
+ if (validSpecialChars.contains(ch)) {
+ SpecialChars.insert(ch);
+ }
+ }
+}
+
+bool TPasswordComplexity::IsSpecialCharValid(char ch) const {
+ return SpecialChars.contains(ch);
+}
+
+const TString TPasswordComplexity::VALID_SPECIAL_CHARS = "!@#$%^&*()_+{}|<>?=";
+
+TPasswordChecker::TComplexityState::TComplexityState(const TPasswordComplexity& passwordComplexity)
+ : PasswordComplexity(passwordComplexity)
+{}
+
+void TPasswordChecker::TComplexityState::IncLowerCaseCount() {
+ ++LowerCaseCount;
+}
+
+void TPasswordChecker::TComplexityState::IncUpperCaseCount() {
+ ++UpperCaseCount;
+}
+
+void TPasswordChecker::TComplexityState::IncNumbersCount() {
+ ++NumbersCount;
+}
+
+void TPasswordChecker::TComplexityState::IncSpecialCharsCount() {
+ ++SpecialCharsCount;
+}
+
+bool TPasswordChecker::TComplexityState::CheckLowerCaseCount() const {
+ return LowerCaseCount >= PasswordComplexity.MinLowerCaseCount;
+}
+
+bool TPasswordChecker::TComplexityState::CheckUpperCaseCount() const {
+ return UpperCaseCount >= PasswordComplexity.MinUpperCaseCount;
+}
+
+bool TPasswordChecker::TComplexityState::CheckNumbersCount() const {
+ return NumbersCount >= PasswordComplexity.MinNumbersCount;
+}
+
+bool TPasswordChecker::TComplexityState::CheckSpecialCharsCount() const {
+ return SpecialCharsCount >= PasswordComplexity.MinSpecialCharsCount;
+}
+
+TPasswordChecker::TPasswordChecker(const TPasswordComplexity& passwordComplexity)
+ : PasswordComplexity(passwordComplexity)
+{}
+
+TPasswordChecker::TResult TPasswordChecker::Check(const TString& username, const TString& password) const {
+ if (password.empty() && PasswordComplexity.MinLength == 0) {
+ return {.Success = true};
+ }
+ if (password.length() < PasswordComplexity.MinLength) {
+ return {.Success = false, .Error = "Password is too short"};
+ }
+ if (!PasswordComplexity.CanContainUsername && password.Contains(username)) {
+ return {.Success = false, .Error = "Password must not contain user name"};
+ }
+
+ TComplexityState complexityState(PasswordComplexity);
+ for (const char& ch : password) {
+ if (std::islower(static_cast<unsigned char>(ch))) {
+ complexityState.IncLowerCaseCount();
+ } else if (std::isupper(static_cast<unsigned char>(ch))) {
+ complexityState.IncUpperCaseCount();
+ } else if (std::isdigit(static_cast<unsigned char>(ch))) {
+ complexityState.IncNumbersCount();
+ } else if (PasswordComplexity.IsSpecialCharValid(ch)) {
+ complexityState.IncSpecialCharsCount();
+ } else {
+ return {.Success = false, .Error = "Password contains unacceptable characters"};
+ }
+ }
+
+ TStringBuilder errorMessage;
+ errorMessage << "Incorrect password format: ";
+ bool hasError = false;
+ if (!complexityState.CheckLowerCaseCount()) {
+ errorMessage << "should contain at least " << PasswordComplexity.MinLowerCaseCount << " lower case character";
+ hasError = true;
+ }
+ if (!complexityState.CheckUpperCaseCount()) {
+ if (hasError) {
+ errorMessage << ", ";
+ }
+ errorMessage << "should contain at least " << PasswordComplexity.MinUpperCaseCount << " upper case character";
+ hasError = true;
+ }
+ if (!complexityState.CheckNumbersCount()) {
+ if (hasError) {
+ errorMessage << ", ";
+ }
+ errorMessage << "should contain at least " << PasswordComplexity.MinNumbersCount << " number";
+ hasError = true;
+ }
+ if (!complexityState.CheckSpecialCharsCount()) {
+ if (hasError) {
+ errorMessage << ", ";
+ }
+ errorMessage << "should contain at least " << PasswordComplexity.MinSpecialCharsCount << " special character";
+ hasError = true;
+ }
+
+ if (hasError) {
+ return {.Success = false, .Error = errorMessage};
+ }
+ return {.Success = true};
+}
+
+void TPasswordChecker::Update(const TPasswordComplexity& passwordComplexity) {
+ PasswordComplexity = passwordComplexity;
+}
+
+} // NLogin
diff --git a/ydb/library/login/password_checker/password_checker.h b/ydb/library/login/password_checker/password_checker.h
new file mode 100644
index 00000000000..3cd25ac659a
--- /dev/null
+++ b/ydb/library/login/password_checker/password_checker.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <util/system/types.h>
+#include <util/generic/string.h>
+#include <unordered_set>
+
+namespace NLogin {
+
+class TPasswordComplexity {
+public:
+ struct TInitializer {
+ size_t MinLength = 0;
+ size_t MinLowerCaseCount = 0;
+ size_t MinUpperCaseCount = 0;
+ size_t MinNumbersCount = 0;
+ size_t MinSpecialCharsCount = 0;
+ TString SpecialChars = VALID_SPECIAL_CHARS;
+ bool CanContainUsername = false;
+ };
+
+ static const TString VALID_SPECIAL_CHARS;
+
+ size_t MinLength = 0;
+ size_t MinLowerCaseCount = 0;
+ size_t MinUpperCaseCount = 0;
+ size_t MinNumbersCount = 0;
+ size_t MinSpecialCharsCount = 0;
+ std::unordered_set<char> SpecialChars;
+ bool CanContainUsername = false;
+
+ TPasswordComplexity();
+ TPasswordComplexity(const TInitializer& initializer);
+
+ bool IsSpecialCharValid(char ch) const;
+};
+
+class TPasswordChecker {
+public:
+ struct TResult {
+ bool Success = true;
+ TString Error;
+ };
+
+private:
+ class TComplexityState {
+ private:
+ size_t LowerCaseCount = 0;
+ size_t UpperCaseCount = 0;
+ size_t NumbersCount = 0;
+ size_t SpecialCharsCount = 0;
+
+ const TPasswordComplexity& PasswordComplexity;
+
+ public:
+ TComplexityState(const TPasswordComplexity& passwordComplexity);
+
+ void IncLowerCaseCount();
+ void IncUpperCaseCount();
+ void IncNumbersCount();
+ void IncSpecialCharsCount();
+
+ bool CheckLowerCaseCount() const;
+ bool CheckUpperCaseCount() const;
+ bool CheckNumbersCount() const;
+ bool CheckSpecialCharsCount() const;
+ };
+
+private:
+ TPasswordComplexity PasswordComplexity;
+
+public:
+ TPasswordChecker(const TPasswordComplexity& passwordComplexity);
+ TResult Check(const TString& username, const TString& password) const;
+ void Update(const TPasswordComplexity& passwordComplexity);
+};
+
+} // NLogin
diff --git a/ydb/library/login/password_checker/password_checker_ut.cpp b/ydb/library/login/password_checker/password_checker_ut.cpp
new file mode 100644
index 00000000000..0f458b7c190
--- /dev/null
+++ b/ydb/library/login/password_checker/password_checker_ut.cpp
@@ -0,0 +1,171 @@
+#include <library/cpp/testing/unittest/registar.h>
+#include <util/string/builder.h>
+#include "password_checker.h"
+
+using namespace NLogin;
+
+Y_UNIT_TEST_SUITE(PasswordChecker) {
+
+ Y_UNIT_TEST(CheckCorrectPasswordWithMaxComplexity) {
+ TPasswordComplexity passwordComplexity({
+ .MinLength = 8,
+ .MinLowerCaseCount = 2,
+ .MinUpperCaseCount = 2,
+ .MinNumbersCount = 2,
+ .MinSpecialCharsCount = 2,
+ .SpecialChars = TPasswordComplexity::VALID_SPECIAL_CHARS,
+ .CanContainUsername = false
+ });
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "qwer%Bs7*S4";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(result.Success, result.Error);
+ UNIT_ASSERT(result.Error.empty());
+ }
+
+ Y_UNIT_TEST(CannotAcceptTooShortPassword) {
+ TPasswordComplexity passwordComplexity({.MinLength = 8});
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "abcd"; // Short password
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, "Password is too short");
+ }
+
+ Y_UNIT_TEST(PasswordCannotContainUsername) {
+ TPasswordComplexity passwordComplexity({.CanContainUsername = false});
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "123testuserqqq";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, "Password must not contain user name");
+ }
+
+ Y_UNIT_TEST(CannotAcceptPasswordWithoutLowerCaseCharacters) {
+ TPasswordComplexity passwordComplexity({
+ .MinLowerCaseCount = 4
+ });
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "12345$*QWERTY";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, TStringBuilder() << "Incorrect password format: should contain at least "
+ << passwordComplexity.MinLowerCaseCount
+ << " lower case character");
+ }
+
+ Y_UNIT_TEST(CannotAcceptPasswordWithoutUpperCaseCharacters) {
+ TPasswordComplexity passwordComplexity({
+ .MinUpperCaseCount = 4
+ });
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "12345$*qwerty";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, TStringBuilder() << "Incorrect password format: should contain at least "
+ << passwordComplexity.MinUpperCaseCount
+ << " upper case character");
+ }
+
+ Y_UNIT_TEST(CannotAcceptPasswordWithoutNumbers) {
+ TPasswordComplexity passwordComplexity({
+ .MinNumbersCount = 4
+ });
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "ASDF$*qwerty";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, TStringBuilder() << "Incorrect password format: should contain at least "
+ << passwordComplexity.MinNumbersCount
+ << " number");
+ }
+
+ Y_UNIT_TEST(CannotAcceptPasswordWithoutSpecialCharacters) {
+ TPasswordComplexity passwordComplexity({
+ .MinSpecialCharsCount = 4
+ });
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "ASDF42qwerty";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, TStringBuilder() << "Incorrect password format: should contain at least "
+ << passwordComplexity.MinSpecialCharsCount
+ << " special character");
+ }
+
+ Y_UNIT_TEST(CannotAcceptPasswordWithInvalidCharacters) {
+ TPasswordComplexity passwordComplexity;
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "ASDF42*qwerty~~"; // ~ is invalid character
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, "Password contains unacceptable characters");
+ }
+
+ Y_UNIT_TEST(CannotAcceptPasswordWithoutLowerCaseAndSpecialCharacters) {
+ TPasswordComplexity passwordComplexity({
+ .MinLowerCaseCount = 2, .MinSpecialCharsCount = 2
+ });
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "ASDF42Q6S7D8";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, TStringBuilder() << "Incorrect password format: should contain at least "
+ << passwordComplexity.MinLowerCaseCount
+ << " lower case character, should contain at least "
+ << passwordComplexity.MinSpecialCharsCount
+ << " special character");
+ }
+
+ Y_UNIT_TEST(AcceptPasswordWithCustomSpecialCharactersList) {
+ TString customSpecialCharacters = "!&*"; // Only 3 special symbols are accepted
+ TPasswordComplexity passwordComplexity({
+ .MinSpecialCharsCount = 2,
+ .SpecialChars = customSpecialCharacters
+ });
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString correctPassword = "pass45!WOR*d";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, correctPassword);
+ UNIT_ASSERT_C(result.Success, result.Error);
+ UNIT_ASSERT(result.Error.empty());
+
+ result = passwordChecker.Check(username, "pass45!WOR$d"); // '$' incorrect symbol
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, "Password contains unacceptable characters");
+ }
+
+ Y_UNIT_TEST(AcceptEmptyPassword) {
+ TPasswordComplexity passwordComplexity({
+ .MinLength = 0
+ }); // Enable empty password by set MinLength as 0
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(result.Success, result.Error);
+ UNIT_ASSERT(result.Error.empty());
+ }
+
+ Y_UNIT_TEST(CannotAcceptEmptyPassword) {
+ TPasswordComplexity passwordComplexity({
+ .MinLength = 8
+ }); // Disable empty password, min length is 8
+ TPasswordChecker passwordChecker(passwordComplexity);
+ TString username = "testuser";
+ TString password = "";
+ TPasswordChecker::TResult result = passwordChecker.Check(username, password);
+ UNIT_ASSERT_C(!result.Success, "Must be error");
+ UNIT_ASSERT_STRINGS_EQUAL(result.Error, "Password is too short");
+ }
+
+}
diff --git a/ydb/library/login/password_checker/ut/ya.make b/ydb/library/login/password_checker/ut/ya.make
new file mode 100644
index 00000000000..cf54044a349
--- /dev/null
+++ b/ydb/library/login/password_checker/ut/ya.make
@@ -0,0 +1,9 @@
+UNITTEST_FOR(ydb/library/login/password_checker)
+
+PEERDIR()
+
+SRCS(
+ password_checker_ut.cpp
+)
+
+END()
diff --git a/ydb/library/login/password_checker/ya.make b/ydb/library/login/password_checker/ya.make
new file mode 100644
index 00000000000..10f403ce7b7
--- /dev/null
+++ b/ydb/library/login/password_checker/ya.make
@@ -0,0 +1,13 @@
+LIBRARY()
+
+PEERDIR()
+
+SRCS(
+ password_checker.cpp
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ ut
+)
diff --git a/ydb/library/login/ya.make b/ydb/library/login/ya.make
index 21a013295ec..e420beb2db9 100644
--- a/ydb/library/login/ya.make
+++ b/ydb/library/login/ya.make
@@ -7,6 +7,7 @@ PEERDIR(
library/cpp/json
library/cpp/string_utils/base64
ydb/library/login/protos
+ ydb/library/login/password_checker
)
SRCS(
@@ -19,3 +20,7 @@ END()
RECURSE_FOR_TESTS(
ut
)
+
+RECURSE(
+ password_checker
+)