diff --git a/ydb/core/protos/feature_flags.proto b/ydb/core/protos/feature_flags.proto index bf4af84de48b..2c45b5cb3c8d 100644 --- a/ydb/core/protos/feature_flags.proto +++ b/ydb/core/protos/feature_flags.proto @@ -126,4 +126,5 @@ message TFeatureFlags { optional bool UseVDisksBalancing = 111 [default = false]; optional bool EnableViews = 112 [default = false]; optional bool EnableServerlessExclusiveDynamicNodes = 113 [default = false]; + optional bool EnableAccessServiceBulkAuthorization = 114 [default = false]; } diff --git a/ydb/core/security/ticket_parser_impl.h b/ydb/core/security/ticket_parser_impl.h index 2dbe4b653e97..18aa1aa0dbf7 100644 --- a/ydb/core/security/ticket_parser_impl.h +++ b/ydb/core/security/ticket_parser_impl.h @@ -48,9 +48,16 @@ class TTicketParserImpl : public TActorBootstrapped { }; struct TPermissionRecord { + enum class TTypeCase { + TYPE_NOT_SET, + USER_ACCOUNT_TYPE, + SERVICE_ACCOUNT_TYPE, + ANONYMOUS_ACCOUNT_TYPE, + }; + TString Subject; bool Required = false; - yandex::cloud::priv::servicecontrol::v1::Subject::TypeCase SubjectType; + TTypeCase SubjectType; TEvTicketParser::TError Error; TStackVec> Attributes; @@ -78,6 +85,7 @@ class TTicketParserImpl : public TActorBootstrapped { using TEvAccessServiceAuthenticateRequest = TEvRequestWithKey; using TEvAccessServiceAuthorizeRequest = TEvRequestWithKey; + using TEvAccessServiceBulkAuthorizeRequest = TEvRequestWithKey; using TEvAccessServiceGetUserAccountRequest = TEvRequestWithKey; using TEvAccessServiceGetServiceAccountRequest = TEvRequestWithKey; @@ -262,7 +270,8 @@ class TTicketParserImpl : public TActorBootstrapped { TDuration LifeTime = TDuration::Hours(1); // for how long ticket will remain in the cache after last access TDuration AsSignatureExpireTime = TDuration::Minutes(1); - TActorId AccessServiceValidator; + TActorId AccessServiceValidatorV1; + TActorId AccessServiceValidatorV2; TActorId UserAccountService; TActorId ServiceAccountService; TString UserAccountDomain; @@ -350,42 +359,68 @@ class TTicketParserImpl : public TActorBootstrapped { return request; } - template - void RequestAccessServiceAuthorization(const TString& key, TTokenRecord& record) const { - for (const auto& [perm, permRecord] : record.Permissions) { - const TString& permission(perm); - BLOG_TRACE("Ticket " << record.GetMaskedTicket() << " asking for AccessServiceAuthorization(" << permission << ")"); + template + void addResourcePaths(const TTokenRecord& record, const TString& permission, TPathsContainerPtr pathsContainer) const { + auto addResourcePath = [&pathsContainer] (const TString& id, const TString& type) { + auto resourcePath = pathsContainer->add_resource_path(); + resourcePath->set_id(id); + resourcePath->set_type(type); + }; - auto request = CreateAccessServiceRequest(key, record); + if (const auto databaseId = record.GetAttributeValue(permission, "database_id"); databaseId) { + addResourcePath(databaseId, "ydb.database"); + } else if (const auto serviceAccountId = record.GetAttributeValue(permission, "service_account_id"); serviceAccountId) { + addResourcePath(serviceAccountId, "iam.serviceAccount"); + } - auto addResourcePath = [&request] (const TString& id, const TString& type) { - auto* resourcePath = request->Request.add_resource_path(); - resourcePath->set_id(id); - resourcePath->set_type(type); - }; + if (const auto folderId = record.GetAttributeValue(permission, "folder_id"); folderId) { + addResourcePath(folderId, "resource-manager.folder"); + } - request->Request.set_permission(permission); + if (const auto cloudId = record.GetAttributeValue(permission, "cloud_id"); cloudId) { + addResourcePath(cloudId, "resource-manager.cloud"); + } - if (const auto databaseId = record.GetAttributeValue(permission, "database_id"); databaseId) { - addResourcePath(databaseId, "ydb.database"); - } else if (const auto serviceAccountId = record.GetAttributeValue(permission, "service_account_id"); serviceAccountId) { - addResourcePath(serviceAccountId, "iam.serviceAccount"); - } + if (const TString gizmoId = record.GetAttributeValue(permission, "gizmo_id"); gizmoId) { + addResourcePath(gizmoId, "iam.gizmo"); + } + } - if (const auto folderId = record.GetAttributeValue(permission, "folder_id"); folderId) { - addResourcePath(folderId, "resource-manager.folder"); - } + template + void AccessServiceAuthorize(const TString& key, TTokenRecord& record) const { + for (const auto& [permissionName, permissionRecord] : record.Permissions) { + BLOG_TRACE("Ticket " << record.GetMaskedTicket() << " asking for AccessServiceAuthorization(" << permissionName << ")"); - if (const auto cloudId = record.GetAttributeValue(permission, "cloud_id"); cloudId) { - addResourcePath(cloudId, "resource-manager.cloud"); - } + auto request = CreateAccessServiceRequest(key, record); + request->Request.set_permission(permissionName); + addResourcePaths(record, permissionName, &request->Request); + record.ResponsesLeft++; + Send(AccessServiceValidatorV1, request.Release()); + } + } - if (const TString gizmoId = record.GetAttributeValue(permission, "gizmo_id"); gizmoId) { - addResourcePath(gizmoId, "iam.gizmo"); - } + template + void AccessServiceBulkAuthorize(const TString& key, TTokenRecord& record) const { + auto request = CreateAccessServiceRequest(key, record); + TStringBuilder requestForPermissions; + for (const auto& [permissionName, permissionRecord] : record.Permissions) { + auto action = request->Request.mutable_actions()->add_items(); + addResourcePaths(record, permissionName, action); + action->set_permission(permissionName); + requestForPermissions << " " << permissionName; + } + request->Request.set_result_filter(yandex::cloud::priv::accessservice::v2::BulkAuthorizeRequest::ALL_FAILED); + BLOG_TRACE("Ticket " << record.GetMaskedTicket() << " asking for AccessServiceBulkAuthorization(" << requestForPermissions << ")"); + record.ResponsesLeft++; + Send(AccessServiceValidatorV2, request.Release()); + } - record.ResponsesLeft++; - Send(AccessServiceValidator, request.Release()); + template + void RequestAccessServiceAuthorization(const TString& key, TTokenRecord& record) const { + if (AppData()->FeatureFlags.GetEnableAccessServiceBulkAuthorization()) { + AccessServiceBulkAuthorize(key, record); + } else { + AccessServiceAuthorize(key, record); } } @@ -396,23 +431,38 @@ class TTicketParserImpl : public TActorBootstrapped { auto request = CreateAccessServiceRequest(key, record); record.ResponsesLeft++; - Send(AccessServiceValidator, request.Release()); + Send(AccessServiceValidatorV1, request.Release()); } - TString GetSubjectName(const yandex::cloud::priv::servicecontrol::v1::Subject& subject) { + template + TString GetSubjectName(const TSubject& subject) { switch (subject.type_case()) { - case yandex::cloud::priv::servicecontrol::v1::Subject::TypeCase::kUserAccount: + case TSubject::TypeCase::kUserAccount: return subject.user_account().id() + "@" + AccessServiceDomain; - case yandex::cloud::priv::servicecontrol::v1::Subject::TypeCase::kServiceAccount: + case TSubject::TypeCase::kServiceAccount: return subject.service_account().id() + "@" + AccessServiceDomain; - case yandex::cloud::priv::servicecontrol::v1::Subject::TypeCase::kAnonymousAccount: + case TSubject::TypeCase::kAnonymousAccount: return "anonymous" "@" + AccessServiceDomain; default: return "Unknown subject type"; } } + template + TPermissionRecord::TTypeCase ConvertSubjectType(const TSubjectType& type) { + switch (type) { + case TSubjectType::kUserAccount: + return TPermissionRecord::TTypeCase::USER_ACCOUNT_TYPE; + case TSubjectType::kServiceAccount: + return TPermissionRecord::TTypeCase::SERVICE_ACCOUNT_TYPE; + case TSubjectType::kAnonymousAccount: + return TPermissionRecord::TTypeCase::ANONYMOUS_ACCOUNT_TYPE; + default: + return TPermissionRecord::TTypeCase::TYPE_NOT_SET; + } + } + template bool CanInitBuiltinToken(const TString& key, TTokenRecord& record) { if (record.TokenType == TDerived::ETokenType::Unknown || record.TokenType == TDerived::ETokenType::Builtin) { @@ -741,6 +791,140 @@ class TTicketParserImpl : public TActorBootstrapped { } } + template + void SetAccessServiceBulkAuthorizeError(const TString& key, TTokenRecord& record, const TString& errorMessage, bool isRetryableError) { + for (auto& [permissionName, permissionRecord] : record.Permissions) { + permissionRecord.Subject.clear(); + permissionRecord.Error = {.Message = errorMessage, .Retryable = isRetryableError}; + BLOG_TRACE("Ticket " << record.GetMaskedTicket() + << " permission " << permissionName + << " now has a " << (isRetryableError ? "retryable" : "permanent") << " error \"" << errorMessage << "\"" + << " retryable: " << isRetryableError); + } + SetError(key, record, {.Message = errorMessage, .Retryable = isRetryableError}); + } + + void Handle(NCloud::TEvAccessService::TEvBulkAuthorizeResponse::TPtr& ev) { + auto getResourcePathIdForRequiredPermissions = [] (const ::yandex::cloud::priv::accessservice::v2::Resource& resource_path) -> TString { + if (resource_path.type() == "resource-manager.folder") { + return " folder_id " + resource_path.id(); + } + if (resource_path.type() == "resource-manager.cloud") { + return " cloud_id " + resource_path.id(); + } + if (resource_path.type() == "iam.serviceAccount") { + return " service_account_id " + resource_path.id(); + } + return ""; + }; + + NCloud::TEvAccessService::TEvBulkAuthorizeResponse* response = ev->Get(); + TEvAccessServiceBulkAuthorizeRequest* request = response->Request->Get(); + const TString& key(request->Key); + auto& userTokens = GetDerived()->GetUserTokens(); + auto itToken = userTokens.find(key); + if (itToken == userTokens.end()) { + BLOG_ERROR("Ticket(key) " + << MaskTicket(key) + << " has expired during permission check"); + } else { + auto& record = itToken->second; + --record.ResponsesLeft; + auto& examinedPermissions = record.Permissions; + if (response->Status.Ok()) { + if (response->Response.has_unauthenticated_error()) { + SetAccessServiceBulkAuthorizeError(key, record, response->Response.unauthenticated_error().message(), false); + } else { + const auto& subject = response->Response.subject(); + const TString subjectName = GetSubjectName(subject); + const auto& subjectType = ConvertSubjectType(subject.type_case()); + for (auto& [permissionName, permissionRecord] : examinedPermissions) { + permissionRecord.Subject = subjectName; + permissionRecord.SubjectType = subjectType; + permissionRecord.Error.clear(); + } + size_t permissionDeniedCount = 0; + bool hasRequiredPermissionFailed = false; + std::vector::iterator> requiredPermissions; + TString permissionDeniedError; + const auto& results = response->Response.results(); + for (const auto& result : results.items()) { + auto permissionDeniedIt = examinedPermissions.find(result.permission()); + if (permissionDeniedIt != examinedPermissions.end()) { + permissionDeniedCount++; + auto& permissionDeniedRecord = permissionDeniedIt->second; + permissionDeniedRecord.Subject.clear(); + BLOG_TRACE("Ticket " << record.GetMaskedTicket() << " permission " << result.permission() << " access denied for subject \"" << subjectName << "\""); + TStringBuilder errorMessage; + if (permissionDeniedRecord.IsRequired()) { + hasRequiredPermissionFailed = true; + errorMessage << permissionDeniedIt->first << " for"; + for (const auto& resourcePath : result.resource_path()) { + errorMessage << getResourcePathIdForRequiredPermissions(resourcePath); + } + errorMessage << " - "; + requiredPermissions.push_back(permissionDeniedIt); + } + permissionDeniedError = result.permission_denied_error().message();; + errorMessage << permissionDeniedError; + permissionDeniedRecord.Error = {.Message = errorMessage, .Retryable = false}; + } else { + BLOG_W("Received response for unknown permission " << result.permission() << " for ticket " << record.GetMaskedTicket()); + } + } + if (permissionDeniedCount < examinedPermissions.size() && !hasRequiredPermissionFailed) { + record.TokenType = request->Request.has_api_key() ? TDerived::ETokenType::ApiKey : TDerived::ETokenType::AccessService; + switch (subjectType) { + case TPermissionRecord::TTypeCase::USER_ACCOUNT_TYPE: + if (UserAccountService) { + BLOG_TRACE("Ticket " << record.GetMaskedTicket() + << " asking for UserAccount(" << subjectName << ")"); + THolder request = MakeHolder(key); + request->Token = record.Ticket; + request->Request.set_user_account_id(subject.user_account().id()); + record.ResponsesLeft++; + Send(UserAccountService, request.Release()); + return; + } + break; + case TPermissionRecord::TTypeCase::SERVICE_ACCOUNT_TYPE: + if (ServiceAccountService) { + BLOG_TRACE("Ticket " << record.GetMaskedTicket() + << " asking for ServiceAccount(" << subjectName << ")"); + THolder request = MakeHolder(key); + request->Token = record.Ticket; + request->Request.set_service_account_id(subject.service_account().id()); + record.ResponsesLeft++; + Send(ServiceAccountService, request.Release()); + return; + } + break; + default: + break; + } + SetToken(request->Key, record, new NACLib::TUserToken(record.Ticket, subjectName, {})); + } else { + if (hasRequiredPermissionFailed) { + TStringBuilder errorMessage; + auto it = requiredPermissions.cbegin(); + errorMessage << (*it)->second.Error.Message; + ++it; + for (; it != requiredPermissions.cend(); ++it) { + errorMessage << ", " << (*it)->second.Error.Message; + } + SetError(key, record, {.Message = errorMessage, .Retryable = false}); + } else { + SetError(key, record, {.Message = permissionDeniedError, .Retryable = false}); + } + } + } + } else { + SetAccessServiceBulkAuthorizeError(key, record, response->Status.Msg, IsRetryableGrpcError(response->Status)); + } + Respond(record); + } + } + void Handle(NCloud::TEvAccessService::TEvAuthorizeResponse::TPtr& ev) { NCloud::TEvAccessService::TEvAuthorizeResponse* response = ev->Get(); TEvAccessServiceAuthorizeRequest* request = response->Request->Get(); @@ -755,12 +939,12 @@ class TTicketParserImpl : public TActorBootstrapped { auto& record = itToken->second; TString permission = request->Request.permission(); TString subject; - yandex::cloud::priv::servicecontrol::v1::Subject::TypeCase subjectType = yandex::cloud::priv::servicecontrol::v1::Subject::TypeCase::TYPE_NOT_SET; + typename TPermissionRecord::TTypeCase subjectType = TPermissionRecord::TTypeCase::TYPE_NOT_SET; auto itPermission = record.Permissions.find(permission); if (itPermission != record.Permissions.end()) { if (response->Status.Ok()) { subject = GetSubjectName(response->Response.subject()); - subjectType = response->Response.subject().type_case(); + subjectType = ConvertSubjectType(response->Response.subject().type_case()); itPermission->second.Subject = subject; itPermission->second.SubjectType = subjectType; itPermission->second.Error.clear(); @@ -836,7 +1020,7 @@ class TTicketParserImpl : public TActorBootstrapped { if (permissionsOk > 0 && retryableErrors == 0 && !requiredPermissionFailed) { record.TokenType = request->Request.has_api_key() ? TDerived::ETokenType::ApiKey : TDerived::ETokenType::AccessService; switch (subjectType) { - case yandex::cloud::priv::servicecontrol::v1::Subject::TypeCase::kUserAccount: + case TPermissionRecord::TTypeCase::USER_ACCOUNT_TYPE: if (UserAccountService) { BLOG_TRACE("Ticket " << record.GetMaskedTicket() << " asking for UserAccount(" << subject << ")"); @@ -848,7 +1032,7 @@ class TTicketParserImpl : public TActorBootstrapped { return; } break; - case yandex::cloud::priv::servicecontrol::v1::Subject::TypeCase::kServiceAccount: + case TPermissionRecord::TTypeCase::SERVICE_ACCOUNT_TYPE: if (ServiceAccountService) { BLOG_TRACE("Ticket " << record.GetMaskedTicket() << " asking for ServiceAccount(" << subject << ")"); @@ -1054,13 +1238,13 @@ class TTicketParserImpl : public TActorBootstrapped { } if (tokenType == "Bearer" || tokenType == "IAM") { - if (AccessServiceValidator) { + if (AccessServiceValidatorV1 && AccessServiceValidatorV2) { return TDerived::ETokenType::AccessService; } else { return TDerived::ETokenType::Unsupported; } } else if (tokenType == "ApiKey") { - if (AccessServiceValidator && Config.GetUseAccessServiceApiKey()) { + if (AccessServiceValidatorV1 && AccessServiceValidatorV2 && Config.GetUseAccessServiceApiKey()) { return TDerived::ETokenType::ApiKey; } else { return TDerived::ETokenType::Unsupported; @@ -1116,7 +1300,7 @@ class TTicketParserImpl : public TActorBootstrapped { template void InitTokenRecord(const TString& key, TTokenRecord& record, TInstant) { if (GetDerived()->CanInitAccessServiceToken(record)) { - if (AccessServiceValidator) { + if (AccessServiceValidatorV1 && AccessServiceValidatorV2) { if (record.Permissions) { RequestAccessServiceAuthorization(key, record); } else { @@ -1278,7 +1462,7 @@ class TTicketParserImpl : public TActorBootstrapped { template bool CanRefreshAccessServiceTicket(const TTokenRecord& record) { - if (!AccessServiceValidator) { + if (!AccessServiceValidatorV1 && AccessServiceValidatorV2) { return false; } if (record.TokenType == TDerived::ETokenType::AccessService || record.TokenType == TDerived::ETokenType::ApiKey) { @@ -1397,7 +1581,7 @@ class TTicketParserImpl : public TActorBootstrapped { void WriteAuthorizeMethods(TStringBuilder& html) { html << "Login" << HtmlBool(UseLoginProvider) << ""; - html << "Access Service" << HtmlBool((bool)AccessServiceValidator) << ""; + html << "Access Service" << HtmlBool((bool)AccessServiceValidatorV1 && (bool)AccessServiceValidatorV2) << ""; html << "User Account Service" << HtmlBool((bool)UserAccountService) << ""; html << "Service Account Service" << HtmlBool((bool)ServiceAccountService) << ""; } @@ -1444,21 +1628,23 @@ class TTicketParserImpl : public TActorBootstrapped { } settings.GrpcKeepAliveTimeMs = Config.GetAccessServiceGrpcKeepAliveTimeMs(); settings.GrpcKeepAliveTimeoutMs = Config.GetAccessServiceGrpcKeepAliveTimeoutMs(); - AccessServiceValidator = Register(NCloud::CreateAccessService(settings), TMailboxType::HTSwap, AppData()->UserPoolId); + AccessServiceValidatorV1 = Register(NCloud::CreateAccessServiceV1(settings), TMailboxType::HTSwap, AppData()->UserPoolId); if (Config.GetCacheAccessServiceAuthentication()) { - AccessServiceValidator = Register(NCloud::CreateGrpcServiceCache( - AccessServiceValidator, + AccessServiceValidatorV1 = Register(NCloud::CreateGrpcServiceCache( + AccessServiceValidatorV1, Config.GetGrpcCacheSize(), TDuration::MilliSeconds(Config.GetGrpcSuccessLifeTime()), TDuration::MilliSeconds(Config.GetGrpcErrorLifeTime())), TMailboxType::HTSwap, AppData()->UserPoolId); } if (Config.GetCacheAccessServiceAuthorization()) { - AccessServiceValidator = Register(NCloud::CreateGrpcServiceCache( - AccessServiceValidator, + AccessServiceValidatorV1 = Register(NCloud::CreateGrpcServiceCache( + AccessServiceValidatorV1, Config.GetGrpcCacheSize(), TDuration::MilliSeconds(Config.GetGrpcSuccessLifeTime()), TDuration::MilliSeconds(Config.GetGrpcErrorLifeTime())), TMailboxType::HTSwap, AppData()->UserPoolId); } + + AccessServiceValidatorV2 = Register(NCloud::CreateAccessServiceV2(settings), TMailboxType::HTSwap, AppData()->UserPoolId); } if (Config.GetUseUserAccountService()) { @@ -1509,8 +1695,11 @@ class TTicketParserImpl : public TActorBootstrapped { } void PassAway() override { - if (AccessServiceValidator) { - Send(AccessServiceValidator, new TEvents::TEvPoisonPill); + if (AccessServiceValidatorV1) { + Send(AccessServiceValidatorV1, new TEvents::TEvPoisonPill); + } + if (AccessServiceValidatorV2) { + Send(AccessServiceValidatorV2, new TEvents::TEvPoisonPill); } if (UserAccountService) { Send(UserAccountService, new TEvents::TEvPoisonPill); @@ -1556,6 +1745,7 @@ class TTicketParserImpl : public TActorBootstrapped { hFunc(TEvLdapAuthProvider::TEvEnrichGroupsResponse, Handle); hFunc(NCloud::TEvAccessService::TEvAuthenticateResponse, Handle); hFunc(NCloud::TEvAccessService::TEvAuthorizeResponse, Handle); + hFunc(NCloud::TEvAccessService::TEvBulkAuthorizeResponse, Handle); hFunc(NCloud::TEvUserAccountService::TEvGetUserAccountResponse, Handle); hFunc(NCloud::TEvServiceAccountService::TEvGetServiceAccountResponse, Handle); hFunc(NMon::TEvHttpInfo, Handle); diff --git a/ydb/core/security/ticket_parser_ut.cpp b/ydb/core/security/ticket_parser_ut.cpp index 9f3fa8b862f7..57e87fd9e1b6 100644 --- a/ydb/core/security/ticket_parser_ut.cpp +++ b/ydb/core/security/ticket_parser_ut.cpp @@ -1158,7 +1158,8 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT_VALUES_EQUAL(result->Token->GetUserSID(), "user1@as"); } - Y_UNIT_TEST(AuthorizationRetryError) { + template + void AuthorizationRetryError() { using namespace Tests; TPortManager tp; @@ -1174,6 +1175,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { authConfig.SetUseStaff(false); authConfig.SetMinErrorRefreshTime("300ms"); auto settings = TServerSettings(port, authConfig); + settings.SetEnableAccessServiceBulkAuthorization(EnableBulkAuthorization); settings.SetDomainName("Root"); settings.CreateTicketParser = NKikimr::CreateTicketParser; TServer server(settings); @@ -1185,7 +1187,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { client.InitRootScheme(); // Access Server Mock - NKikimr::TAccessServiceMock accessServiceMock; + TAccessServiceMock accessServiceMock; grpc::ServerBuilder builder; builder.AddListeningPort(accessServiceEndpoint, grpc::InsecureServerCredentials()).RegisterService(&accessServiceMock); std::unique_ptr accessServer(builder.BuildAndStart()); @@ -1220,7 +1222,16 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT(!result->Token->IsExist("something.write-bbbb4554@as")); } - Y_UNIT_TEST(AuthorizationRetryErrorImmediately) { + Y_UNIT_TEST(AuthorizationRetryError) { + AuthorizationRetryError(); + } + + Y_UNIT_TEST(BulkAuthorizationRetryError) { + AuthorizationRetryError(); + } + + template + void AuthorizationRetryErrorImmediately() { using namespace Tests; TPortManager tp; @@ -1236,6 +1247,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { authConfig.SetUseStaff(false); authConfig.SetRefreshPeriod("5s"); auto settings = TServerSettings(port, authConfig); + settings.SetEnableAccessServiceBulkAuthorization(EnableBulkAuthorization); settings.SetDomainName("Root"); settings.CreateTicketParser = NKikimr::CreateTicketParser; TServer server(settings); @@ -1247,7 +1259,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { client.InitRootScheme(); // Access Server Mock - NKikimr::TAccessServiceMock accessServiceMock; + TAccessServiceMock accessServiceMock; grpc::ServerBuilder builder; builder.AddListeningPort(accessServiceEndpoint, grpc::InsecureServerCredentials()).RegisterService(&accessServiceMock); std::unique_ptr accessServer(builder.BuildAndStart()); @@ -1280,6 +1292,14 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT(!result->Token->IsExist("something.write-bbbb4554@as")); } + Y_UNIT_TEST(AuthorizationRetryErrorImmediately) { + AuthorizationRetryErrorImmediately(); + } + + Y_UNIT_TEST(BulkAuthorizationRetryErrorImmediately) { + AuthorizationRetryErrorImmediately(); + } + Y_UNIT_TEST(AuthenticationUnsupported) { using namespace Tests; @@ -1371,7 +1391,8 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT_VALUES_EQUAL(result->Error.Message, "Unknown token"); } - Y_UNIT_TEST(Authorization) { + template + void Authorization() { using namespace Tests; TPortManager tp; @@ -1387,6 +1408,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { authConfig.SetAccessServiceEndpoint(accessServiceEndpoint); authConfig.SetUseStaff(false); auto settings = TServerSettings(port, authConfig); + settings.SetEnableAccessServiceBulkAuthorization(EnableBulkAuthorization); settings.SetDomainName("Root"); settings.CreateTicketParser = NKikimr::CreateTicketParser; TServer server(settings); @@ -1400,7 +1422,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { TString userToken = "user1"; // Access Server Mock - NKikimr::TAccessServiceMock accessServiceMock; + TAccessServiceMock accessServiceMock; grpc::ServerBuilder builder; builder.AddListeningPort(accessServiceEndpoint, grpc::InsecureServerCredentials()).RegisterService(&accessServiceMock); std::unique_ptr accessServer(builder.BuildAndStart()); @@ -1419,6 +1441,18 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT(result->Token->IsExist("something.read-bbbb4554@as")); UNIT_ASSERT(!result->Token->IsExist("something.write-bbbb4554@as")); + accessServiceMock.AllowedUserPermissions.insert("user1-something.connect"); + runtime->Send(new IEventHandle(MakeTicketParserID(), sender, new TEvTicketParser::TEvAuthorizeTicket( + userToken, + {{"folder_id", "aaaa1234"}, {"database_id", "bbbb4554"}}, + {"something.read", "something.connect", "something.list", "something.update"})), 0); + result = runtime->GrabEdgeEvent(handle); + UNIT_ASSERT(result->Error.empty()); + UNIT_ASSERT(result->Token->IsExist("something.read-bbbb4554@as")); + UNIT_ASSERT(result->Token->IsExist("something.connect-bbbb4554@as")); + UNIT_ASSERT(!result->Token->IsExist("something.list-bbbb4554@as")); + UNIT_ASSERT(!result->Token->IsExist("something.update-bbbb4554@as")); + // Authorization ApiKey successful. runtime->Send(new IEventHandle(MakeTicketParserID(), sender, new TEvTicketParser::TEvAuthorizeTicket( "ApiKey ApiKey-value-valid", @@ -1514,7 +1548,16 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT(result->Token->IsExist("monitoring.view-gizmo@as")); } - Y_UNIT_TEST(AuthorizationWithRequiredPermissions) { + Y_UNIT_TEST(Authorization) { + Authorization(); + } + + Y_UNIT_TEST(BulkAuthorization) { + Authorization(); + } + + template + void AuthorizationWithRequiredPermissions() { using namespace Tests; TPortManager tp; @@ -1529,6 +1572,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { authConfig.SetAccessServiceEndpoint(accessServiceEndpoint); authConfig.SetUseStaff(false); auto settings = TServerSettings(port, authConfig); + settings.SetEnableAccessServiceBulkAuthorization(EnableBulkAuthorization); settings.SetDomainName("Root"); settings.CreateTicketParser = NKikimr::CreateTicketParser; TServer server(settings); @@ -1542,7 +1586,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { TString userToken = "user1"; // Access Server Mock - NKikimr::TAccessServiceMock accessServiceMock; + TAccessServiceMock accessServiceMock; grpc::ServerBuilder builder; builder.AddListeningPort(accessServiceEndpoint, grpc::InsecureServerCredentials()).RegisterService(&accessServiceMock); std::unique_ptr accessServer(builder.BuildAndStart()); @@ -1572,7 +1616,16 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT_VALUES_EQUAL(result->Error.Message, "something.write for folder_id aaaa1234 - Access Denied"); } - Y_UNIT_TEST(AuthorizationWithUserAccount) { + Y_UNIT_TEST(AuthorizationWithRequiredPermissions) { + AuthorizationWithRequiredPermissions(); + } + + Y_UNIT_TEST(BulkAuthorizationWithRequiredPermissions) { + AuthorizationWithRequiredPermissions(); + } + + template + void AuthorizationWithUserAccount() { using namespace Tests; TPortManager tp; @@ -1593,6 +1646,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { authConfig.SetCacheAccessServiceAuthorization(false); // auto settings = TServerSettings(port, authConfig); + settings.SetEnableAccessServiceBulkAuthorization(EnableBulkAuthorization); settings.SetDomainName("Root"); settings.CreateTicketParser = NKikimr::CreateTicketParser; TServer server(settings); @@ -1606,7 +1660,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { TString userToken = "user1"; // Access Server Mock - NKikimr::TAccessServiceMock accessServiceMock; + TAccessServiceMock accessServiceMock; grpc::ServerBuilder builder1; builder1.AddListeningPort(accessServiceEndpoint, grpc::InsecureServerCredentials()).RegisterService(&accessServiceMock); std::unique_ptr accessServer(builder1.BuildAndStart()); @@ -1670,7 +1724,16 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT_VALUES_EQUAL(result->Token->GetUserSID(), "login1@passport"); } - Y_UNIT_TEST(AuthorizationWithUserAccount2) { + Y_UNIT_TEST(AuthorizationWithUserAccount) { + AuthorizationWithUserAccount(); + } + + Y_UNIT_TEST(BulkAuthorizationWithUserAccount) { + AuthorizationWithUserAccount(); + } + + template + void AuthorizationWithUserAccount2() { using namespace Tests; TPortManager tp; @@ -1688,6 +1751,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { authConfig.SetUseUserAccountServiceTLS(false); authConfig.SetUserAccountServiceEndpoint(userAccountServiceEndpoint); auto settings = TServerSettings(port, authConfig); + settings.SetEnableAccessServiceBulkAuthorization(EnableBulkAuthorization); settings.SetDomainName("Root"); settings.CreateTicketParser = NKikimr::CreateTicketParser; TServer server(settings); @@ -1701,7 +1765,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { TString userToken = "user1"; // Access Server Mock - NKikimr::TAccessServiceMock accessServiceMock; + TAccessServiceMock accessServiceMock; grpc::ServerBuilder builder1; builder1.AddListeningPort(accessServiceEndpoint, grpc::InsecureServerCredentials()).RegisterService(&accessServiceMock); std::unique_ptr accessServer(builder1.BuildAndStart()); @@ -1735,7 +1799,16 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT_VALUES_EQUAL(result->Token->GetUserSID(), "login1@passport"); } - Y_UNIT_TEST(AuthorizationUnavailable) { + Y_UNIT_TEST(AuthorizationWithUserAccount2) { + AuthorizationWithUserAccount2(); + } + + Y_UNIT_TEST(BulkAuthorizationWithUserAccount2) { + AuthorizationWithUserAccount2(); + } + + template + void AuthorizationUnavailable() { using namespace Tests; TPortManager tp; @@ -1750,6 +1823,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { authConfig.SetAccessServiceEndpoint(accessServiceEndpoint); authConfig.SetUseStaff(false); auto settings = TServerSettings(port, authConfig); + settings.SetEnableAccessServiceBulkAuthorization(EnableBulkAuthorization); settings.SetDomainName("Root"); settings.CreateTicketParser = NKikimr::CreateTicketParser; TServer server(settings); @@ -1763,7 +1837,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { TString userToken = "user1"; // Access Server Mock - NKikimr::TAccessServiceMock accessServiceMock; + TAccessServiceMock accessServiceMock; grpc::ServerBuilder builder; builder.AddListeningPort(accessServiceEndpoint, grpc::InsecureServerCredentials()).RegisterService(&accessServiceMock); std::unique_ptr accessServer(builder.BuildAndStart()); @@ -1785,7 +1859,16 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT_VALUES_EQUAL(result->Error.Message, "Service Unavailable"); } - Y_UNIT_TEST(AuthorizationModify) { + Y_UNIT_TEST(AuthorizationUnavailable) { + AuthorizationUnavailable(); + } + + Y_UNIT_TEST(BulkAuthorizationUnavailable) { + AuthorizationUnavailable(); + } + + template + void AuthorizationModify() { using namespace Tests; TPortManager tp; @@ -1800,6 +1883,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { authConfig.SetAccessServiceEndpoint(accessServiceEndpoint); authConfig.SetUseStaff(false); auto settings = TServerSettings(port, authConfig); + settings.SetEnableAccessServiceBulkAuthorization(EnableBulkAuthorization); settings.SetDomainName("Root"); settings.CreateTicketParser = NKikimr::CreateTicketParser; TServer server(settings); @@ -1813,7 +1897,7 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { TString userToken = "user1"; // Access Server Mock - NKikimr::TAccessServiceMock accessServiceMock; + TAccessServiceMock accessServiceMock; grpc::ServerBuilder builder; builder.AddListeningPort(accessServiceEndpoint, grpc::InsecureServerCredentials()).RegisterService(&accessServiceMock); std::unique_ptr accessServer(builder.BuildAndStart()); @@ -1845,5 +1929,13 @@ Y_UNIT_TEST_SUITE(TTicketParserTest) { UNIT_ASSERT(result->Token->IsExist("something.read-bbbb4554@as")); UNIT_ASSERT(result->Token->IsExist("something.write-bbbb4554@as")); } + + Y_UNIT_TEST(AuthorizationModify) { + AuthorizationModify(); + } + + Y_UNIT_TEST(BulkAuthorizationModify) { + AuthorizationModify(); + } } } diff --git a/ydb/core/testlib/basics/feature_flags.h b/ydb/core/testlib/basics/feature_flags.h index 32b44d6c7193..f390430bce63 100644 --- a/ydb/core/testlib/basics/feature_flags.h +++ b/ydb/core/testlib/basics/feature_flags.h @@ -56,6 +56,7 @@ class TTestFeatureFlagsHolder { FEATURE_FLAG_SETTER(EnableUuidAsPrimaryKey) FEATURE_FLAG_SETTER(EnableTablePgTypes) FEATURE_FLAG_SETTER(EnableServerlessExclusiveDynamicNodes) + FEATURE_FLAG_SETTER(EnableAccessServiceBulkAuthorization) #undef FEATURE_FLAG_SETTER }; diff --git a/ydb/library/testlib/service_mocks/access_service_mock.h b/ydb/library/testlib/service_mocks/access_service_mock.h index 927f7fdc4807..5d6f421472ee 100644 --- a/ydb/library/testlib/service_mocks/access_service_mock.h +++ b/ydb/library/testlib/service_mocks/access_service_mock.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -186,3 +187,99 @@ class TTicketParserAccessServiceMock : public yandex::cloud::priv::servicecontro } } }; + +class TTicketParserAccessServiceMockV2 : public yandex::cloud::priv::accessservice::v2::AccessService::Service { +public: + std::atomic_uint64_t AuthorizeCount= 0; + bool ShouldGenerateRetryableError = false; + bool ShouldGenerateOneRetryableError = false; + bool isUserAuthenticated = true; + + THashSet UnavailableUserPermissions; + THashSet AllowedResourceIds; + THashSet AllowedUserPermissions = { + "user1-something.read", + "ApiKey-value-valid-something.read", + "ApiKey-value-valid-ydb.api.kafkaPlainAuth", + "user1-monitoring.view" + }; + THashMap AllowedServicePermissions = {{"service1-something.write", "root1/folder1"}}; + +public: + ::grpc::Status BulkAuthorize(::grpc::ServerContext*, + const ::yandex::cloud::priv::accessservice::v2::BulkAuthorizeRequest* request, + ::yandex::cloud::priv::accessservice::v2::BulkAuthorizeResponse* response) { + ++AuthorizeCount; + if (request->has_signature()) { + if (ShouldGenerateRetryableError) { + return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Service Unavailable"); + } + if (ShouldGenerateOneRetryableError) { + ShouldGenerateOneRetryableError = false; + return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Service Unavailable"); + } + response->mutable_subject()->mutable_user_account()->set_id("user1"); + return grpc::Status::OK; + } else { + if (!isUserAuthenticated) { + auto error = response->mutable_unauthenticated_error(); + error->set_message("Access Denied"); + return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Access Denied"); + } + TString token = request->has_iam_token() ? request->iam_token() : request->api_key(); + if (request->has_actions()) { + const auto& actions = request->actions(); + bool wasFoundFirstAccessDenied = false; + for (const auto& action : actions.items()) { + if (UnavailableUserPermissions.count(token + '-' + action.permission()) > 0) { + return grpc::Status(grpc::StatusCode::UNAVAILABLE, "Service Unavailable"); + } + + bool allowedResource = true; + if (!AllowedResourceIds.empty()) { + allowedResource = false; + for (const auto& resourcePath : action.resource_path()) { + if (AllowedResourceIds.count(resourcePath.id()) > 0) { + allowedResource = true; + } + } + } + if (allowedResource) { + if (AllowedUserPermissions.count(token + '-' + action.permission()) > 0) { + response->mutable_subject()->mutable_user_account()->set_id(token); + + } else if (AllowedServicePermissions.count(token + '-' + action.permission()) > 0) { + response->mutable_subject()->mutable_service_account()->set_id(token); + response->mutable_subject()->mutable_service_account()->set_folder_id(AllowedServicePermissions[token + '-' + action.permission()]); + } else { + if (request->result_filter() == yandex::cloud::priv::accessservice::v2::BulkAuthorizeRequest::ALL_FAILED) { + SetAccessDenied(response->mutable_results(), action); + } else { + if (!wasFoundFirstAccessDenied) { + SetAccessDenied(response->mutable_results(), action); + wasFoundFirstAccessDenied = true; + } + } + } + } else { + SetAccessDenied(response->mutable_results(), action); + } + } + } + return grpc::Status(grpc::StatusCode::OK, "OK"); + } + } + +private: + void SetAccessDenied(::yandex::cloud::priv::accessservice::v2::BulkAuthorizeResponse_Results* results, + const ::yandex::cloud::priv::accessservice::v2::BulkAuthorizeRequest_Action& action) { + auto result = results->add_items(); + result->set_permission(action.permission()); + for (const auto& resource_path : action.resource_path()) { + auto rp = result->add_resource_path(); + rp->CopyFrom(resource_path); + } + auto error = result->mutable_permission_denied_error(); + error->set_message("Access Denied"); + } +}; diff --git a/ydb/library/testlib/service_mocks/ya.make b/ydb/library/testlib/service_mocks/ya.make index 9bf784d56f6a..a88798023332 100644 --- a/ydb/library/testlib/service_mocks/ya.make +++ b/ydb/library/testlib/service_mocks/ya.make @@ -13,6 +13,7 @@ SRCS( PEERDIR( ydb/public/api/client/yc_private/servicecontrol + ydb/public/api/client/yc_private/accessservice ydb/public/api/grpc/draft ydb/public/api/client/yc_private/resourcemanager ydb/public/api/client/yc_private/iam diff --git a/ydb/library/ycloud/api/access_service.h b/ydb/library/ycloud/api/access_service.h index 67149a8e1077..2c60a82d9e64 100644 --- a/ydb/library/ycloud/api/access_service.h +++ b/ydb/library/ycloud/api/access_service.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "events.h" namespace NCloud { @@ -12,10 +13,12 @@ namespace NCloud { // requests EvAuthenticateRequest = EventSpaceBegin(TKikimrEvents::ES_ACCESS_SERVICE), EvAuthorizeRequest, + EvBulkAuthorizeRequest, // replies EvAuthenticateResponse = EventSpaceBegin(TKikimrEvents::ES_ACCESS_SERVICE) + 512, EvAuthorizeResponse, + EvBulkAuthorizeResponse, EvEnd }; @@ -29,5 +32,8 @@ namespace NCloud { struct TEvAuthorizeRequest : TEvGrpcProtoRequest {}; struct TEvAuthorizeResponse : TEvGrpcProtoResponse {}; + + struct TEvBulkAuthorizeRequest : TEvGrpcProtoRequest {}; + struct TEvBulkAuthorizeResponse : TEvGrpcProtoResponse {}; }; } diff --git a/ydb/library/ycloud/api/ya.make b/ydb/library/ycloud/api/ya.make index 9348f2dfb375..bec439527582 100644 --- a/ydb/library/ycloud/api/ya.make +++ b/ydb/library/ycloud/api/ya.make @@ -11,6 +11,7 @@ SRCS( PEERDIR( ydb/public/api/client/yc_private/iam ydb/public/api/client/yc_private/servicecontrol + ydb/public/api/client/yc_private/accessservice ydb/public/api/client/yc_private/resourcemanager ydb/library/actors/core ydb/library/grpc/client diff --git a/ydb/library/ycloud/impl/access_service.cpp b/ydb/library/ycloud/impl/access_service.cpp index 1678f459afcf..97ab06e485f4 100644 --- a/ydb/library/ycloud/impl/access_service.cpp +++ b/ydb/library/ycloud/impl/access_service.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "access_service.h" #include "grpc_service_client.h" #include "grpc_service_cache.h" @@ -10,9 +11,9 @@ namespace NCloud { using namespace NKikimr; -class TAccessService : public NActors::TActor, TGrpcServiceClient { - using TThis = TAccessService; - using TBase = NActors::TActor; +class TAccessServiceV1 : public NActors::TActor, TGrpcServiceClient { + using TThis = TAccessServiceV1; + using TBase = NActors::TActor; struct TAuthenticateRequest : TGrpcRequest { static constexpr auto Request = &yandex::cloud::priv::servicecontrol::v1::AccessService::Stub::AsyncAuthenticate; @@ -68,7 +69,7 @@ class TAccessService : public NActors::TActor, TGrpcServiceClien public: static constexpr NKikimrServices::TActivity::EType ActorActivityType() { return NKikimrServices::TActivity::ACCESS_SERVICE_ACTOR; } - TAccessService(const TAccessServiceSettings& settings) + TAccessServiceV1(const TAccessServiceSettings& settings) : TBase(&TThis::StateWork) , TGrpcServiceClient(settings) {} @@ -82,13 +83,62 @@ class TAccessService : public NActors::TActor, TGrpcServiceClien } }; +class TAccessServiceV2 : public NActors::TActor, TGrpcServiceClient { + using TThis = TAccessServiceV2; + using TBase = NActors::TActor; -IActor* CreateAccessService(const TAccessServiceSettings& settings) { - return new TAccessService(settings); + struct TBulkAuthorizeRequest : TGrpcRequest { + static constexpr auto Request = &yandex::cloud::priv::accessservice::v2::AccessService::Stub::AsyncBulkAuthorize; + using TRequestEventType = TEvAccessService::TEvBulkAuthorizeRequest; + using TResponseEventType = TEvAccessService::TEvBulkAuthorizeResponse; + + static yandex::cloud::priv::accessservice::v2::BulkAuthorizeRequest Obfuscate(const yandex::cloud::priv::accessservice::v2::BulkAuthorizeRequest& p) { + yandex::cloud::priv::accessservice::v2::BulkAuthorizeRequest r(p); + if (r.iam_token()) { + r.set_iam_token(MaskToken(r.iam_token())); + } + if (r.api_key()) { + r.set_api_key(MaskToken(r.api_key())); + } + return r; + } + + static const yandex::cloud::priv::accessservice::v2::BulkAuthorizeResponse& Obfuscate(const yandex::cloud::priv::accessservice::v2::BulkAuthorizeResponse& p) { + return p; + } + }; + + void Handle(TEvAccessService::TEvBulkAuthorizeRequest::TPtr& ev) { + MakeCall(std::move(ev)); + } + +public: + static constexpr NKikimrServices::TActivity::EType ActorActivityType() { return NKikimrServices::TActivity::ACCESS_SERVICE_ACTOR; } + + TAccessServiceV2(const TAccessServiceSettings& settings) + : TBase(&TThis::StateWork) + , TGrpcServiceClient(settings) + {} + + void StateWork(TAutoPtr& ev) { + switch (ev->GetTypeRewrite()) { + hFunc(TEvAccessService::TEvBulkAuthorizeRequest, Handle); + cFunc(TEvents::TSystem::PoisonPill, PassAway); + } + } +}; + + +IActor* CreateAccessServiceV1(const TAccessServiceSettings& settings) { + return new TAccessServiceV1(settings); +} + +IActor* CreateAccessServiceV2(const TAccessServiceSettings& settings) { + return new TAccessServiceV2(settings); } IActor* CreateAccessServiceWithCache(const TAccessServiceSettings& settings) { - IActor* accessService = CreateAccessService(settings); + IActor* accessService = CreateAccessServiceV1(settings); accessService = CreateGrpcServiceCache(accessService); accessService = CreateGrpcServiceCache(accessService); return accessService; diff --git a/ydb/library/ycloud/impl/access_service.h b/ydb/library/ycloud/impl/access_service.h index bdcf2b1c0037..ee1cb03c8d75 100644 --- a/ydb/library/ycloud/impl/access_service.h +++ b/ydb/library/ycloud/impl/access_service.h @@ -8,12 +8,19 @@ using namespace NKikimr; struct TAccessServiceSettings : TGrpcClientSettings {}; -IActor* CreateAccessService(const TAccessServiceSettings& settings); +IActor* CreateAccessServiceV1(const TAccessServiceSettings& settings); +IActor* CreateAccessServiceV2(const TAccessServiceSettings& settings); -inline IActor* CreateAccessService(const TString& endpoint) { +inline IActor* CreateAccessServiceV1(const TString& endpoint) { TAccessServiceSettings settings; settings.Endpoint = endpoint; - return CreateAccessService(settings); + return CreateAccessServiceV1(settings); +} + +inline IActor* CreateAccessServiceV2(const TString& endpoint) { + TAccessServiceSettings settings; + settings.Endpoint = endpoint; + return CreateAccessServiceV2(settings); } IActor* CreateAccessServiceWithCache(const TAccessServiceSettings& settings); // for compatibility with older code diff --git a/ydb/library/ycloud/impl/mock_access_service.cpp b/ydb/library/ycloud/impl/mock_access_service.cpp index 1e2f44cc204f..1f518213028f 100644 --- a/ydb/library/ycloud/impl/mock_access_service.cpp +++ b/ydb/library/ycloud/impl/mock_access_service.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include "access_service.h" #include "grpc_service_client.h" #include "grpc_service_cache.h" diff --git a/ydb/public/api/client/yc_private/accessservice/access_service.proto b/ydb/public/api/client/yc_private/accessservice/access_service.proto new file mode 100644 index 000000000000..83d7fa9e0b85 --- /dev/null +++ b/ydb/public/api/client/yc_private/accessservice/access_service.proto @@ -0,0 +1,259 @@ +syntax = "proto3"; + +package yandex.cloud.priv.accessservice.v2; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/field_mask.proto"; +import "ydb/public/api/client/yc_private/accessservice/resource.proto"; +import "ydb/public/api/client/yc_private/accessservice/sensitive.proto"; + +option go_package = "accessservice_v2"; +option java_outer_classname = "PAS"; + +service AccessService { + // Verify the identity of a subject. + // + // gRPC error codes + // + // Ok: the provided credentials are valid + // Unauthenticated: the provided credentials are invalid or may have expired + // InvalidArgument: the client specified an invalid argument (please note that this applies to the request in itself, + // not to the content of the request, i.e. you will get the InvalidArgument error if the message + // size exceeds the server limit but Unauthenticated if the token format is not recognized) + // Unavailable: the service is currently unavailable, the client should retry again + // Internal: the service is broken + // + // Please note that these do not include client-side errors (e.g. Cancelled, DeadlineExceeded, etc.) + rpc Authenticate (AuthenticateRequest) returns (AuthenticateResponse); + + // Check if a subject is allowed to perform an action. This also authenticates the subject if any credentials are + // passed as an identity. + // + // gRPC error codes + // + // Ok: the provided credentials (if any) are valid and the subject has permissions to access the + // specified resource + // Unauthenticated: the provided credentials are invalid or may have expired + // PermissionDenied: the subject does not have permissions to access the specified resource + // InvalidArgument: the client specified an invalid argument (please note that this applies to the request in itself, + // not to the content of the request, i.e. you will get the InvalidArgument error if the message + // size exceeds the server limit or the specified permission does not exist but Unauthenticated if + // the token format is not recognized) + // Unavailable: the service is currently unavailable, the client should retry again + // Internal: the service is broken + // + // Please note that these do not include client-side errors (e.g. Cancelled, DeadlineExceeded, etc.) + rpc Authorize (AuthorizeRequest) returns (AuthorizeResponse); + + // Similar to Authorize, but requests multiple actions for one subject. + // + // gRPC error codes will be the same, except for these cases: + // - An Unauthenticated error of BulkAuthorizeRequest.identity is returned in + // BulkAuthorizeResponse.unauthenticated_error. + // - All PermissionDenied of BulkAuthorizeRequest.authorizations are returned in + // BulkAuthorizeResponse.results. + // + // You can control the information returned in BulkAuthorizeResponse.results with: + // - result_filter : return all errors (ALL_FAILED) or only the first one (FIRST_FAILED), if any. + // - result_mask : You can choose the fields returned (all by default), + // from the fields in BulkAuthorizeResponse.Result. + // + rpc BulkAuthorize (BulkAuthorizeRequest) returns (BulkAuthorizeResponse); +} + +message AuthenticateRequest { + oneof credentials { + // option (exactly_one) = true; + + // IAM-token obtained from the IAM Token Service. + // The server response for an empty IAM token is UNAUTHENTICATED + string iam_token = 1 [(sensitive) = true, (sensitive_type) = SENSITIVE_IAM_TOKEN]; // [(length) = "<=1024"]; + + // AWS-compatible signature. + AccessKeySignature signature = 2; + + // API key. + // The server response for an empty API key is UNAUTHENTICATED + string api_key = 3 [(sensitive) = true, (sensitive_type) = SENSITIVE_CRC]; + + // IAM-cookie. + // The server response for an empty IAM cookie is UNAUTHENTICATED + string iam_cookie = 4 [(sensitive) = true, (sensitive_type) = SENSITIVE_IAM_COOKIE]; + + // RefreshToken. + // The server response for an empty RefreshToken is UNAUTHENTICATED + string refresh_token = 5 [(sensitive) = true, (sensitive_type) = SENSITIVE_REFRESH_TOKEN]; + } +} + +message AuthenticateResponse { + Subject subject = 1; +} + +message AuthorizeRequest { + oneof identity { + // option (exactly_one) = true; + + Subject subject = 1; + + // IAM-token obtained from the IAM Token Service. + // The server response for an empty IAM token is UNAUTHENTICATED + string iam_token = 2 [(sensitive) = true, (sensitive_type) = SENSITIVE_IAM_TOKEN]; // [(length) = "<=1024"]; + + // AWS-compatible signature. + AccessKeySignature signature = 3; + + // API key. + // The server response for an empty API key is UNAUTHENTICATED + string api_key = 6 [(sensitive) = true, (sensitive_type) = SENSITIVE_CRC]; + } + + string permission = 4; // [(required) = true, (length) = "<=128"]; + + // A resource to authorize access to. This may also include a service-specific hierarchy of the resource, usually + // ends with resource-manager.folder. + // + // Examples: + // (resource-manager.folder, b1gn3enigctah04o0fkb) + // (billing.account, b1gqql62454n46tboesn) + // (compute.instance, b1gqqhvc4fg65mkrefs8), (resource-manager.folder, b1gn3enigctah04o0fkb) + // (resource-manager.cloud, aje56o8prppkrpaiuoc6) + // (my-service.instance, b1gqqepv0upu57issrog), (resource-manager.cloud, aje56o8prppkrpaiuoc6) + repeated Resource resource_path = 5; // [(size) = ">0"]; +} + +message AuthorizeResponse { + Subject subject = 1; + + // Full path to the resource. + repeated Resource resource_path = 2; +} + +message BulkAuthorizeRequest { + oneof identity { + // option (exactly_one) = true; + + Subject subject = 1; + + string iam_token = 2 [(sensitive) = true, (sensitive_type) = SENSITIVE_IAM_TOKEN]; // [(length) = "<=1024"]; + + AccessKeySignature signature = 3; + + string api_key = 4 [(sensitive) = true, (sensitive_type) = SENSITIVE_CRC]; + } + + oneof authorizations { + // option (exactly_one) = true; + + Actions actions = 5; + + ActionMatrix action_matrix = 6; + } + + ResultFilter result_filter = 7; + + google.protobuf.FieldMask result_mask = 8; + + message Action { + repeated Resource resource_path = 1; // [(size) = "1-128"]; + + string permission = 2; // [(required) = true, (length) = "<=128"]; + } + + message Actions { + repeated Action items = 1; // [(size) = "1-1000"]; + } + + // Cross product of paths and permissions (represents N*M actions, N*M <= 1000). + message ActionMatrix { + repeated ResourcePath resource_paths = 2; // [(size) = "1-1000"]; + + repeated string permissions = 1; // [(size) = "1-1000", (length) = "<=128"]; + } + + enum ResultFilter { + RESULT_FILTER_UNSPECIFIED = 0; + FIRST_FAILED = 1; + ALL_FAILED = 2; + } +} + +message BulkAuthorizeResponse { + Subject subject = 1; + + Error unauthenticated_error = 2; + + Results results = 3; + + message Results { + repeated Result items = 1; + } + + message Result { + string permission = 1; + + repeated Resource resource_path = 2; + + Error permission_denied_error = 3; + } + + message Error { + string message = 1; + + repeated google.protobuf.Any details = 2; + } +} + +message AccessKeySignature { + string access_key_id = 1; // [(required) = true, (length) = "<=50"]; + string string_to_sign = 2; // [(required) = true, (length) = "<=8192"]; + string signature = 3 [(sensitive) = true, (sensitive_type) = SENSITIVE_CRC]; // [(required) = true, (length) = "<=128"]; + + oneof parameters { + // option (exactly_one) = true; + + Version2Parameters v2_parameters = 4; + Version4Parameters v4_parameters = 5; + } + + message Version2Parameters { + SignatureMethod signature_method = 1; + + enum SignatureMethod { + SIGNATURE_METHOD_UNSPECIFIED = 0; + HMAC_SHA1 = 1; + HMAC_SHA256 = 2; + } + } + + message Version4Parameters { + google.protobuf.Timestamp signed_at = 1; // [(required) = true]; + string service = 2; // [(required) = true, (length) = "<=64"]; + string region = 3; // [(required) = true, (length) = "<=32"]; + } +} + +message Subject { + oneof type { + // option (exactly_one) = true; + + UserAccount user_account = 1; + ServiceAccount service_account = 2; + AnonymousAccount anonymous_account = 3; + } + + message UserAccount { + string id = 1; // [(required) = true, (length) = "<=50"]; + string federation_id = 2; // [(length) = "<=50"]; + } + + message ServiceAccount { + string id = 1; // [(required) = true, (length) = "<=50"]; + string folder_id = 2; // [(length) = "<=50"]; + } + + // Use this if you want to check if an unauthenticated subject is allowed to access a resource. + message AnonymousAccount { + } +} diff --git a/ydb/public/api/client/yc_private/accessservice/resource.proto b/ydb/public/api/client/yc_private/accessservice/resource.proto new file mode 100644 index 000000000000..a20fb36cf1e8 --- /dev/null +++ b/ydb/public/api/client/yc_private/accessservice/resource.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package yandex.cloud.priv.accessservice.v2; + +option go_package = "accessservice_v2"; +option java_outer_classname = "PR"; + +message Resource { + string id = 1; // [(required) = true, (length) = "<=50"]; + + // The type of the resource, e.g. resource-manager.folder, billing.account, compute.snapshot, etc. + string type = 2; // [(required) = true, (length) = "<=64"]; +} + +message ResourcePath { + repeated Resource path = 1; // [(size) = "1-128"]; +} diff --git a/ydb/public/api/client/yc_private/accessservice/sensitive.proto b/ydb/public/api/client/yc_private/accessservice/sensitive.proto new file mode 100644 index 000000000000..009620bd9914 --- /dev/null +++ b/ydb/public/api/client/yc_private/accessservice/sensitive.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +// Based on: +// https://bb.yandexcloud.net/projects/CLOUD/repos/cloud-go/browse/private-api/yandex/cloud/priv/sensitive.proto + +package yandex.cloud; + +import "google/protobuf/descriptor.proto"; + +option go_package = "cloud/proto_extensions"; + +enum SensitiveType { + SENSITIVE_TYPE_UNSPECIFIED = 0; + SENSITIVE_CRC = 1; + SENSITIVE_IAM_TOKEN = 2; + SENSITIVE_REMOVE = 3; + SENSITIVE_YANDEX_PASSPORT_OAUTH_TOKEN = 4; + SENSITIVE_IAM_COOKIE = 5; + SENSITIVE_REFRESH_TOKEN = 6; + SENSITIVE_SESSION_TOKEN = 7; +} + +extend google.protobuf.FieldOptions { + // novikoff: + // Sensitive fields are hidden in logs + // For now could be applied only to string fields + bool sensitive = 110601; + SensitiveType sensitive_type = 110602; +} diff --git a/ydb/public/api/client/yc_private/accessservice/ya.make b/ydb/public/api/client/yc_private/accessservice/ya.make new file mode 100644 index 000000000000..8199005d2336 --- /dev/null +++ b/ydb/public/api/client/yc_private/accessservice/ya.make @@ -0,0 +1,16 @@ +PROTO_LIBRARY() + +EXCLUDE_TAGS(GO_PROTO) + +GRPC() +SRCS( + access_service.proto + resource.proto + sensitive.proto +) + +USE_COMMON_GOOGLE_APIS( + api/annotations +) + +END() diff --git a/ydb/public/api/client/yc_private/servicecontrol/ya.make b/ydb/public/api/client/yc_private/servicecontrol/ya.make index 1820f9a20b96..15a84d453dd9 100644 --- a/ydb/public/api/client/yc_private/servicecontrol/ya.make +++ b/ydb/public/api/client/yc_private/servicecontrol/ya.make @@ -13,4 +13,3 @@ USE_COMMON_GOOGLE_APIS( ) END() -