diff --git a/ydb/core/protos/auth.proto b/ydb/core/protos/auth.proto index e7e741f56317..6f25ed339529 100644 --- a/ydb/core/protos/auth.proto +++ b/ydb/core/protos/auth.proto @@ -105,6 +105,10 @@ message TLdapAuthentication { optional TCertRequire CertRequire = 3 [default = DEMAND]; } + message TExtendedSettings { + optional bool EnableNestedGroupsSearch = 1 [default = false]; + } + optional string Host = 1; // DEPRECATED: Use Hosts instead it optional uint32 Port = 2; optional string BaseDn = 3; @@ -116,4 +120,5 @@ message TLdapAuthentication { optional string RequestedGroupAttribute = 9; repeated string Hosts = 10; optional string Scheme = 11 [default = "ldap"]; + optional TExtendedSettings ExtendedSettings = 12; } diff --git a/ydb/core/security/ldap_auth_provider/ldap_auth_provider.cpp b/ydb/core/security/ldap_auth_provider/ldap_auth_provider.cpp index 7d4173141fb0..2f6c28697967 100644 --- a/ydb/core/security/ldap_auth_provider/ldap_auth_provider.cpp +++ b/ydb/core/security/ldap_auth_provider/ldap_auth_provider.cpp @@ -149,7 +149,8 @@ class TLdapAuthProvider : public NActors::TActorBootstrapped NKikimrLdap::BerFree(ber, 0); } std::vector allUserGroups; - if (!directUserGroups.empty()) { + auto& extendedSettings = Settings.GetExtendedSettings(); + if (extendedSettings.GetEnableNestedGroupsSearch() && !directUserGroups.empty()) { // Active Directory has special matching rule to fetch nested groups in one request it is MatchingRuleInChain // We don`t know what is ldap server. Is it Active Directory or OpenLdap or other server? // If using MatchingRuleInChain return empty list of groups it means that ldap server isn`t Active Directory @@ -159,6 +160,8 @@ class TLdapAuthProvider : public NActors::TActorBootstrapped allUserGroups = std::move(directUserGroups); GetNestedGroups(ld, &allUserGroups); } + } else { + allUserGroups = std::move(directUserGroups); } NKikimrLdap::MsgFree(entry); NKikimrLdap::Unbind(ld); diff --git a/ydb/core/security/ldap_auth_provider/ldap_auth_provider_ut.cpp b/ydb/core/security/ldap_auth_provider/ldap_auth_provider_ut.cpp index e526717e1186..1795ee5752fd 100644 --- a/ydb/core/security/ldap_auth_provider/ldap_auth_provider_ut.cpp +++ b/ydb/core/security/ldap_auth_provider/ldap_auth_provider_ut.cpp @@ -47,6 +47,8 @@ void InitLdapSettings(NKikimrProto::TLdapAuthentication* ldapSettings, ui16 ldap ldapSettings->SetBindDn("cn=robouser,dc=search,dc=yandex,dc=net"); ldapSettings->SetBindPassword("robouserPassword"); ldapSettings->SetSearchFilter("uid=$username"); + auto extendedSettings = ldapSettings->MutableExtendedSettings(); + extendedSettings->SetEnableNestedGroupsSearch(true); const auto setCertificate = [&ldapSettings] (bool useStartTls, TTempFileHandle& certificateFile) { auto useTls = ldapSettings->MutableUseTls(); @@ -64,6 +66,12 @@ void InitLdapSettings(NKikimrProto::TLdapAuthentication* ldapSettings, ui16 ldap } } +void InitLdapSettingsDisableSearchNestedGroups(NKikimrProto::TLdapAuthentication* ldapSettings, ui16 ldapPort, TTempFileHandle& certificateFile, const ESecurityConnectionType& securityConnectionType) { + InitLdapSettings(ldapSettings, ldapPort, certificateFile, securityConnectionType); + auto extendedSettings = ldapSettings->MutableExtendedSettings(); + extendedSettings->SetEnableNestedGroupsSearch(false); +} + void InitLdapSettingsWithInvalidRobotUserLogin(NKikimrProto::TLdapAuthentication* ldapSettings, ui16 ldapPort, TTempFileHandle& certificateFile, const ESecurityConnectionType& securityConnectionType) { InitLdapSettings(ldapSettings, ldapPort, certificateFile, securityConnectionType); ldapSettings->SetBindDn("cn=invalidRobouser,dc=search,dc=yandex,dc=net"); @@ -207,9 +215,9 @@ class TCorrectLdapResponse { static std::vector ManagerGroups; static std::vector DevelopersGroups; static std::vector PeopleGroups; - static LdapMock::TLdapMockResponses GetResponses(const TString& login, const TString& groupAttribute = "memberOf"); - static LdapMock::TLdapMockResponses GetAdResponses(const TString& login, const TString& groupAttribute = "memberOf"); - static LdapMock::TLdapMockResponses GetUpdatedResponses(const TString& login, const TString& groupAttribute = "memberOf"); + static LdapMock::TLdapMockResponses GetResponses(const TString& login, bool doReturnDirectedGroups = false, const TString& groupAttribute = "memberOf"); + static LdapMock::TLdapMockResponses GetAdResponses(const TString& login, bool doReturnDirectedGroups = false, const TString& groupAttribute = "memberOf"); + static LdapMock::TLdapMockResponses GetUpdatedResponses(const TString& login, bool doReturnDirectedGroups = false, const TString& groupAttribute = "memberOf"); static THashSet GetAllGroups(const TString& domain) { THashSet result; auto AddGroups = [&result, &domain] (const std::vector& groups) { @@ -224,6 +232,17 @@ class TCorrectLdapResponse { return result; } + static THashSet GetDirectedGroups(const TString& domain) { + THashSet result; + auto AddGroups = [&result, &domain] (const std::vector& groups) { + std::transform(groups.begin(), groups.end(), std::inserter(result, result.end()), [&domain](const TString& group) { + return TString(group).append(domain); + }); + }; + AddGroups(DirectGroups); + return result; + } + static THashSet GetAllUpdatedGroups(const TString& domain) { THashSet result; auto AddGroups = [&result, &domain] (const std::vector& groups) { @@ -236,6 +255,17 @@ class TCorrectLdapResponse { AddGroups(PeopleGroups); return result; } + + static THashSet GetUpdatedDirectedGroups(const TString& domain) { + THashSet result; + auto AddGroups = [&result, &domain] (const std::vector& groups) { + std::transform(groups.begin(), groups.end(), std::inserter(result, result.end()), [&domain](const TString& group) { + return TString(group).append(domain); + }); + }; + AddGroups(UpdatedDirectGroups); + return result; + } }; std::vector TCorrectLdapResponse::DirectGroups { @@ -259,7 +289,7 @@ std::vector TCorrectLdapResponse::PeopleGroups { "cn=people,ou=groups,dc=search,dc=yandex,dc=net", }; -LdapMock::TLdapMockResponses TCorrectLdapResponse::GetResponses(const TString& login, const TString& groupAttribute) { +LdapMock::TLdapMockResponses TCorrectLdapResponse::GetResponses(const TString& login, bool doReturnDirectedGroups, const TString& groupAttribute) { LdapMock::TLdapMockResponses responses; responses.BindResponses.push_back({{{.Login = "cn=robouser,dc=search,dc=yandex,dc=net", .Password = "robouserPassword"}}, {.Status = LdapMock::EStatus::SUCCESS}}); @@ -288,6 +318,10 @@ LdapMock::TLdapMockResponses TCorrectLdapResponse::GetResponses(const TString& l }; responses.SearchResponses.push_back({requestDirectedUserGroups, responseDirectedUserGroups}); + if (doReturnDirectedGroups) { + return responses; + } + std::shared_ptr filterToGetGroupOfManagers = std::make_shared(); filterToGetGroupOfManagers->Type = LdapMock::EFilterType::LDAP_FILTER_EQUALITY; filterToGetGroupOfManagers->Attribute = "entryDn"; @@ -425,7 +459,7 @@ LdapMock::TLdapMockResponses TCorrectLdapResponse::GetResponses(const TString& l return responses; } -LdapMock::TLdapMockResponses TCorrectLdapResponse::GetUpdatedResponses(const TString& login, const TString& groupAttribute) { +LdapMock::TLdapMockResponses TCorrectLdapResponse::GetUpdatedResponses(const TString& login, bool doReturnDirectedGroups, const TString& groupAttribute) { LdapMock::TLdapMockResponses responses; responses.BindResponses.push_back({{{.Login = "cn=robouser,dc=search,dc=yandex,dc=net", .Password = "robouserPassword"}}, {.Status = LdapMock::EStatus::SUCCESS}}); @@ -454,6 +488,10 @@ LdapMock::TLdapMockResponses TCorrectLdapResponse::GetUpdatedResponses(const TSt }; responses.SearchResponses.push_back({requestDirectedUserGroups, responseDirectedUserGroups}); + if (doReturnDirectedGroups) { + return responses; + } + std::shared_ptr filterToGetGroupOfDevelopers = std::make_shared(); filterToGetGroupOfDevelopers->Type = LdapMock::EFilterType::LDAP_FILTER_EQUALITY; filterToGetGroupOfDevelopers->Attribute = "entryDn"; @@ -568,7 +606,7 @@ LdapMock::TLdapMockResponses TCorrectLdapResponse::GetUpdatedResponses(const TSt return responses; } -LdapMock::TLdapMockResponses TCorrectLdapResponse::GetAdResponses(const TString& login, const TString& groupAttribute) { +LdapMock::TLdapMockResponses TCorrectLdapResponse::GetAdResponses(const TString& login, bool doReturnDirectedGroups, const TString& groupAttribute) { LdapMock::TLdapMockResponses responses; responses.BindResponses.push_back({{{.Login = "cn=robouser,dc=search,dc=yandex,dc=net", .Password = "robouserPassword"}}, {.Status = LdapMock::EStatus::SUCCESS}}); @@ -597,6 +635,10 @@ LdapMock::TLdapMockResponses TCorrectLdapResponse::GetAdResponses(const TString& }; responses.SearchResponses.push_back({requestDirectedUserGroups, responseDirectedUserGroups}); + if (doReturnDirectedGroups) { + return responses; + } + LdapMock::TSearchRequestInfo requestToGetAllNestedGroupsFromAd { { .BaseDn = "dc=search,dc=yandex,dc=net", @@ -692,6 +734,33 @@ void CheckRequiredLdapSettings(std::function handle = LdapAuthenticate(server, login, password); + TEvTicketParser::TEvAuthorizeTicketResult* ticketParserResult = handle->Get(); + UNIT_ASSERT_C(ticketParserResult->Error.empty(), ticketParserResult->Error); + UNIT_ASSERT(ticketParserResult->Token != nullptr); + const TString ldapDomain = "@ldap"; + UNIT_ASSERT_VALUES_EQUAL(ticketParserResult->Token->GetUserSID(), login + ldapDomain); + const auto& fetchedGroups = ticketParserResult->Token->GetGroupSIDs(); + THashSet groups(fetchedGroups.begin(), fetchedGroups.end()); + + THashSet expectedGroups = TCorrectLdapResponse::GetDirectedGroups(ldapDomain); + expectedGroups.insert("all-users@well-known"); + + UNIT_ASSERT_VALUES_EQUAL(fetchedGroups.size(), expectedGroups.size()); + for (const auto& expectedGroup : expectedGroups) { + UNIT_ASSERT_C(groups.contains(expectedGroup), "Can not find " + expectedGroup); + } + + ldapServer.Stop(); + } + void LdapFetchGroupsFromAdLdapServer(const ESecurityConnectionType& secureType) { TString login = "ldapuser"; TString password = "ldapUserPassword"; @@ -719,6 +788,33 @@ void CheckRequiredLdapSettings(std::function handle = LdapAuthenticate(server, login, password); + TEvTicketParser::TEvAuthorizeTicketResult* ticketParserResult = handle->Get(); + UNIT_ASSERT_C(ticketParserResult->Error.empty(), ticketParserResult->Error); + UNIT_ASSERT(ticketParserResult->Token != nullptr); + const TString ldapDomain = "@ldap"; + UNIT_ASSERT_VALUES_EQUAL(ticketParserResult->Token->GetUserSID(), login + ldapDomain); + const auto& fetchedGroups = ticketParserResult->Token->GetGroupSIDs(); + THashSet groups(fetchedGroups.begin(), fetchedGroups.end()); + + THashSet expectedGroups = TCorrectLdapResponse::GetDirectedGroups(ldapDomain); + expectedGroups.insert("all-users@well-known"); + + UNIT_ASSERT_VALUES_EQUAL(fetchedGroups.size(), expectedGroups.size()); + for (const auto& expectedGroup : expectedGroups) { + UNIT_ASSERT_C(groups.contains(expectedGroup), "Can not find " + expectedGroup); + } + + ldapServer.Stop(); + } + void LdapFetchGroupsWithDefaultGroupAttributeGoodUseListOfHosts(const ESecurityConnectionType& secureType) { TString login = "ldapuser"; TString password = "ldapUserPassword"; @@ -751,7 +847,7 @@ void CheckRequiredLdapSettings(std::function handle = LdapAuthenticate(server, login, password); TEvTicketParser::TEvAuthorizeTicketResult* ticketParserResult = handle->Get(); @@ -914,7 +1010,6 @@ void CheckRequiredLdapSettings(std::functionAllocateEdgeActor(); + runtime->Send(new IEventHandle(MakeTicketParserID(), sender, new TEvTicketParser::TEvAuthorizeTicket(loginResponse.Token)), 0); + TAutoPtr handle; + TEvTicketParser::TEvAuthorizeTicketResult* ticketParserResult = runtime->GrabEdgeEvent(handle); + + UNIT_ASSERT_C(ticketParserResult->Error.empty(), ticketParserResult->Error); + UNIT_ASSERT(ticketParserResult->Token != nullptr); + UNIT_ASSERT_VALUES_EQUAL(ticketParserResult->Token->GetUserSID(), login + ldapDomain); + const auto& fetchedGroups = ticketParserResult->Token->GetGroupSIDs(); + THashSet groups(fetchedGroups.begin(), fetchedGroups.end()); + + THashSet expectedGroups = TCorrectLdapResponse::GetDirectedGroups(ldapDomain); + expectedGroups.insert("all-users@well-known"); + + UNIT_ASSERT_VALUES_EQUAL(fetchedGroups.size(), expectedGroups.size()); + for (const auto& expectedGroup : expectedGroups) { + UNIT_ASSERT_C(groups.contains(expectedGroup), "Can not find " + expectedGroup); + } + + ldapServer.UpdateResponses(); + Sleep(TDuration::Seconds(10)); + + runtime->Send(new IEventHandle(MakeTicketParserID(), sender, new TEvTicketParser::TEvAuthorizeTicket(loginResponse.Token)), 0); + ticketParserResult = runtime->GrabEdgeEvent(handle); + + UNIT_ASSERT_C(ticketParserResult->Error.empty(), ticketParserResult->Error); + UNIT_ASSERT(ticketParserResult->Token != nullptr); + UNIT_ASSERT_VALUES_EQUAL(ticketParserResult->Token->GetUserSID(), login + "@ldap"); + const auto& newFetchedGroups = ticketParserResult->Token->GetGroupSIDs(); + THashSet newGroups(newFetchedGroups.begin(), newFetchedGroups.end()); + + THashSet newExpectedGroups = TCorrectLdapResponse::GetUpdatedDirectedGroups(ldapDomain); + newExpectedGroups.insert("all-users@well-known"); + + UNIT_ASSERT_VALUES_EQUAL(newFetchedGroups.size(), newExpectedGroups.size()); + for (const auto& expectedGroup : newExpectedGroups) { + UNIT_ASSERT_C(newGroups.contains(expectedGroup), "Can not find " + expectedGroup); + } + + ldapServer.Stop(); + } + void LdapRefreshRemoveUserBad(const ESecurityConnectionType& secureType) { TString login = "ldapuser"; TString password = "ldapUserPassword"; @@ -1045,10 +1195,18 @@ Y_UNIT_TEST_SUITE(LdapAuthProviderTest_LdapsScheme) { LdapFetchGroupsFromAdLdapServer(ESecurityConnectionType::LDAPS_SCHEME); } + Y_UNIT_TEST(LdapFetchGroupsDisableRequestToAD) { + LdapFetchGroupsDisableRequestToAD(ESecurityConnectionType::LDAPS_SCHEME); + } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeGood) { LdapFetchGroupsWithDefaultGroupAttributeGood(ESecurityConnectionType::LDAPS_SCHEME); } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeDisableNestedGroupsGood) { + LdapFetchGroupsWithDefaultGroupAttributeDisableNestedGroupsGood(ESecurityConnectionType::LDAPS_SCHEME); + } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeGoodUseListOfHosts) { LdapFetchGroupsWithDefaultGroupAttributeGoodUseListOfHosts(ESecurityConnectionType::LDAPS_SCHEME); } @@ -1081,6 +1239,10 @@ Y_UNIT_TEST_SUITE(LdapAuthProviderTest_LdapsScheme) { LdapRefreshGroupsInfoGood(ESecurityConnectionType::LDAPS_SCHEME); } + Y_UNIT_TEST(LdapRefreshGroupsInfoDisableNestedGroupsGood) { + LdapRefreshGroupsInfoDisableNestedGroupsGood(ESecurityConnectionType::LDAPS_SCHEME); + } + Y_UNIT_TEST(LdapRefreshRemoveUserBad) { LdapRefreshRemoveUserBad(ESecurityConnectionType::LDAPS_SCHEME); } @@ -1091,10 +1253,18 @@ Y_UNIT_TEST_SUITE(LdapAuthProviderTest_StartTls) { LdapFetchGroupsFromAdLdapServer(ESecurityConnectionType::START_TLS); } + Y_UNIT_TEST(LdapFetchGroupsDisableRequestToAD) { + LdapFetchGroupsDisableRequestToAD(ESecurityConnectionType::START_TLS); + } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeGood) { LdapFetchGroupsWithDefaultGroupAttributeGood(ESecurityConnectionType::START_TLS); } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeDisableNestedGroupsGood) { + LdapFetchGroupsWithDefaultGroupAttributeDisableNestedGroupsGood(ESecurityConnectionType::START_TLS); + } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeGoodUseListOfHosts) { LdapFetchGroupsWithDefaultGroupAttributeGoodUseListOfHosts(ESecurityConnectionType::START_TLS); } @@ -1127,6 +1297,10 @@ Y_UNIT_TEST_SUITE(LdapAuthProviderTest_StartTls) { LdapRefreshGroupsInfoGood(ESecurityConnectionType::START_TLS); } + Y_UNIT_TEST(LdapRefreshGroupsInfoDisableNestedGroupsGood) { + LdapRefreshGroupsInfoDisableNestedGroupsGood(ESecurityConnectionType::START_TLS); + } + Y_UNIT_TEST(LdapRefreshRemoveUserBad) { LdapRefreshRemoveUserBad(ESecurityConnectionType::START_TLS); } @@ -1137,10 +1311,18 @@ Y_UNIT_TEST_SUITE(LdapAuthProviderTest_nonSecure) { LdapFetchGroupsFromAdLdapServer(ESecurityConnectionType::NON_SECURE); } + Y_UNIT_TEST(LdapFetchGroupsDisableRequestToAD) { + LdapFetchGroupsDisableRequestToAD(ESecurityConnectionType::NON_SECURE); + } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeGood) { LdapFetchGroupsWithDefaultGroupAttributeGood(ESecurityConnectionType::NON_SECURE); } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeDisableNestedGroupsGood) { + LdapFetchGroupsWithDefaultGroupAttributeDisableNestedGroupsGood(ESecurityConnectionType::NON_SECURE); + } + Y_UNIT_TEST(LdapFetchGroupsWithDefaultGroupAttributeGoodUseListOfHosts) { LdapFetchGroupsWithDefaultGroupAttributeGoodUseListOfHosts(ESecurityConnectionType::NON_SECURE); } @@ -1173,6 +1355,10 @@ Y_UNIT_TEST_SUITE(LdapAuthProviderTest_nonSecure) { LdapRefreshGroupsInfoGood(ESecurityConnectionType::NON_SECURE); } + Y_UNIT_TEST(LdapRefreshGroupsInfoDisableNestedGroupsGood) { + LdapRefreshGroupsInfoDisableNestedGroupsGood(ESecurityConnectionType::NON_SECURE); + } + Y_UNIT_TEST(LdapRefreshRemoveUserBad) { LdapRefreshRemoveUserBad(ESecurityConnectionType::NON_SECURE); }