diff --git a/include/ydb-cpp-sdk/client/helpers/helpers.h b/include/ydb-cpp-sdk/client/helpers/helpers.h index e9ed90ed65..ce1a1fd63c 100644 --- a/include/ydb-cpp-sdk/client/helpers/helpers.h +++ b/include/ydb-cpp-sdk/client/helpers/helpers.h @@ -9,6 +9,7 @@ namespace NYdb { //! YDB_ANONYMOUS_CREDENTIALS="1" — uses anonymous access (used for test installation), //! YDB_METADATA_CREDENTIALS="1" — uses metadata service, //! YDB_ACCESS_TOKEN_CREDENTIALS= — access token (for example, IAM-token). +//! YDB_OAUTH2_KEY_FILE= - OAuth 2.0 RFC8693 token exchange credentials parameters json file //! If grpcs protocol is given in endpoint (or protocol is empty), enables SSL and uses //! certificate from resourses and user cert from env variable "YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS" TDriverConfig CreateFromEnvironment(const std::string& connectionString = ""); diff --git a/include/ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/credentials.h b/include/ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/credentials.h index 72aa395f51..3c6017cb71 100644 --- a/include/ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/credentials.h +++ b/include/ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/credentials.h @@ -41,7 +41,7 @@ struct TOauth2TokenExchangeParams { FLUENT_SETTING_DEFAULT(std::string, GrantType, "urn:ietf:params:oauth:grant-type:token-exchange"); - FLUENT_SETTING(std::string, Resource); + FLUENT_SETTING_VECTOR_OR_SINGLE(std::string, Resource); FLUENT_SETTING_VECTOR_OR_SINGLE(std::string, Audience); FLUENT_SETTING_VECTOR_OR_SINGLE(std::string, Scope); diff --git a/include/ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/from_file.h b/include/ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/from_file.h new file mode 100644 index 0000000000..b887cca588 --- /dev/null +++ b/include/ydb-cpp-sdk/client/types/credentials/oauth2_token_exchange/from_file.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include + +namespace NYdb { + +// Lists supported algorithms for creation of OAuth 2.0 token exchange provider via config file +std::vector GetSupportedOauth2TokenExchangeJwtAlgorithms(); + +// Creates OAuth 2.0 token exchange credentials provider factory that exchanges token using standard protocol +// https://www.rfc-editor.org/rfc/rfc8693 +// +// Config file must be a valid json file +// +// Fields of json file +// grant-type: [string] Grant type option (default: see TOauth2TokenExchangeParams) +// res: [string | list of strings] Resource option (optional) +// aud: [string | list of strings] Audience option for token exchange request (optional) +// scope: [string | list of strings] Scope option (optional) +// requested-token-type: [string] Requested token type option (default: see TOauth2TokenExchangeParams) +// subject-credentials: [creds_json] Subject credentials options (optional) +// actor-credentials: [creds_json] Actor credentials options (optional) +// token-endpoint: [string] Token endpoint. Can be overritten with tokenEndpoint param (if it is not empty) +// +// Fields of creds_json (JWT): +// type: [string] Token source type. Set JWT +// alg: [string] Algorithm for JWT signature. Supported algorithms can be listed with GetSupportedOauth2TokenExchangeJwtAlgorithms() +// private-key: [string] (Private) key in PEM format for JWT signature +// kid: [string] Key id JWT standard claim (optional) +// iss: [string] Issuer JWT standard claim (optional) +// sub: [string] Subject JWT standard claim (optional) +// aud: [string | list of strings] Audience JWT standard claim (optional) +// jti: [string] JWT ID JWT standard claim (optional) +// ttl: [string] Token TTL (default: see TJwtTokenSourceParams) +// +// Fields of creds_json (FIXED): +// type: [string] Token source type. Set FIXED +// token: [string] Token value +// token-type: [string] Token type value. It will become subject_token_type/actor_token_type parameter in token exchange request (https://www.rfc-editor.org/rfc/rfc8693) +// +std::shared_ptr CreateOauth2TokenExchangeFileCredentialsProviderFactory(const std::string& configFilePath, const std::string& tokenEndpoint = {}); + +} // namespace NYdb diff --git a/src/api/grpc/draft/ydb_ymq_v1.proto b/src/api/grpc/draft/ydb_ymq_v1.proto new file mode 100644 index 0000000000..2d67a2b646 --- /dev/null +++ b/src/api/grpc/draft/ydb_ymq_v1.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; +option cc_enable_arenas = true; + +package Ydb.Ymq.V1; + +import "src/api/protos/draft/ymq.proto"; + +option java_package = "com.yandex.ydb.ymq.v1"; + +service YmqService { + rpc GetQueueUrl(GetQueueUrlRequest) returns (GetQueueUrlResponse); + rpc CreateQueue(CreateQueueRequest) returns (CreateQueueResponse); + rpc SendMessage(SendMessageRequest) returns (SendMessageResponse); + rpc ReceiveMessage(ReceiveMessageRequest) returns (ReceiveMessageResponse); + rpc GetQueueAttributes(GetQueueAttributesRequest) returns (GetQueueAttributesResponse); + rpc ListQueues(ListQueuesRequest) returns (ListQueuesResponse); + rpc DeleteMessage(DeleteMessageRequest) returns (DeleteMessageResponse); + rpc PurgeQueue(PurgeQueueRequest) returns (PurgeQueueResponse); + rpc DeleteQueue(DeleteQueueRequest) returns (DeleteQueueResponse); + rpc ChangeMessageVisibility(ChangeMessageVisibilityRequest) returns (ChangeMessageVisibilityResponse); +} diff --git a/src/api/protos/draft/datastreams.proto b/src/api/protos/draft/datastreams.proto index bdb8d47f18..d047efc43f 100644 --- a/src/api/protos/draft/datastreams.proto +++ b/src/api/protos/draft/datastreams.proto @@ -2,25 +2,13 @@ syntax = "proto3"; option cc_enable_arenas = true; import "src/api/protos/ydb_operation.proto"; +import "src/api/protos/draft/field_transformation.proto"; import "google/protobuf/descriptor.proto"; package Ydb.DataStreams.V1; option java_package = "com.yandex.ydb.datastreams.v1"; -// Extensions to simplify json <-> proto conversion -enum EFieldTransformationType { - TRANSFORM_NONE = 0; - TRANSFORM_BASE64 = 1; - TRANSFORM_DOUBLE_S_TO_INT_MS = 2; - TRANSFORM_EMPTY_TO_NOTHING = 3; -} - - -extend google.protobuf.FieldOptions { - EFieldTransformationType FieldTransformer = 58123; -} - // Here and below: Kinesis data types mapped to protobuf enum EncryptionType { @@ -53,7 +41,7 @@ message ChildShard { // Represents details of consumer message Consumer { string consumer_arn = 1; - int64 consumer_creation_timestamp = 2 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 consumer_creation_timestamp = 2 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; string consumer_name = 3; ConsumerDescription.ConsumerStatus consumer_status = 4; } @@ -66,9 +54,9 @@ message HashKeyRange { message Record { // Timestamp that the record was inserted into the stream - int64 approximate_arrival_timestamp = 1 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 approximate_arrival_timestamp = 1 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; // Data blob - bytes data = 2 [(FieldTransformer) = TRANSFORM_BASE64]; + bytes data = 2 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_BASE64]; // Encryption type used on record EncryptionType encryption_type = 3; // Identifies shard in the stream the record is assigned to @@ -108,7 +96,7 @@ message StreamDescription { repeated Shard shards = 6; string stream_arn = 7; // Timestamp that the stream was created - int64 stream_creation_timestamp = 8 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 stream_creation_timestamp = 8 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; // Current status of the stream StreamStatus stream_status = 9; // Name of the stream @@ -127,17 +115,17 @@ message StreamDescription { // Represents range of possible sequence numbers for the shard message SequenceNumberRange { string starting_sequence_number = 1; - string ending_sequence_number = 2 [(FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; + string ending_sequence_number = 2 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; } // Represents shard details message Shard { // Id of the shard adjacent to the shard's parent - string adjacent_parent_shard_id = 1 [(FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; + string adjacent_parent_shard_id = 1 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; // The range of possible hash key values for the shard HashKeyRange hash_key_range = 2; // Id of the shard's parent - string parent_shard_id = 3 [(FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; + string parent_shard_id = 3 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; // The range of possible sequence numbers for the shard SequenceNumberRange sequence_number_range = 4; // Unique id of the shard within stream @@ -155,7 +143,7 @@ message ConsumerDescription { string consumer_arn = 1; // Timestamp that the consumer was created - int64 consumer_creation_timestamp = 2 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 consumer_creation_timestamp = 2 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; // Name of the consumer string consumer_name = 3; ConsumerStatus consumer_status = 4; @@ -198,14 +186,14 @@ message ShardFilter { // Exclusive id. Can only be used if AFTER_SHARD_ID is specified string shard_id = 1; // Can only be used if AT_TIMESTAMP or FROM_TIMESTAMP are specified. - int64 timestamp = 2 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 timestamp = 2 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; ShardFilterType type = 3; } // Represents starting position in the stream from which to start reading message StartingPosition { // Timestamp of the record from which to start reading - int64 timestamp = 1 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 timestamp = 1 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; // Sequence number of the record from which to start reading string sequence_number = 2; ShardIteratorType type = 3; @@ -227,7 +215,7 @@ message StreamDescriptionSummary { // Stream ARN string stream_arn = 7; // Timestamp that the stream was created - int64 stream_creation_timestamp = 8 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 stream_creation_timestamp = 8 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; // Name of the stream string stream_name = 9; // Current status of the stream @@ -351,7 +339,7 @@ message ListShardsRequest { // Filter out response ShardFilter shard_filter = 5; // Used to distinguish streams that have the same name - int64 stream_creation_timestamp = 6 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 stream_creation_timestamp = 6 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; // Name of the stream string stream_name = 7; } @@ -417,7 +405,7 @@ message ListStreamConsumersRequest { string next_token = 3; string stream_arn = 4; // Used to distinguish streams that have the same name - int64 stream_creation_timestamp = 5 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 stream_creation_timestamp = 5 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; } message ListStreamConsumersResponse { @@ -502,7 +490,7 @@ message DescribeStreamConsumerResult { message PutRecordsRequestEntry { // Data blob - bytes data = 1 [(FieldTransformer) = TRANSFORM_BASE64]; + bytes data = 1 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_BASE64]; // Hash value used to explicitly determine the shard string explicit_hash_key = 2; // Used as input to hash function that maps partition key to a specific shard @@ -511,8 +499,8 @@ message PutRecordsRequestEntry { // Represents result of an individual record message PutRecordsResultEntry { - string error_message = 2 [(FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; - string error_code = 3 [(FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; + string error_message = 2 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; + string error_code = 3 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_EMPTY_TO_NOTHING]; string sequence_number = 4; string shard_id = 5; } @@ -544,7 +532,7 @@ message GetRecordsResult { message PutRecordRequest { Ydb.Operations.OperationParams operation_params = 1; // Data blob - bytes data = 2 [(FieldTransformer) = TRANSFORM_BASE64]; + bytes data = 2 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_BASE64]; // Hash value used to explicitly determine the shard string explicit_hash_key = 3; // Used as input to hash function that maps partition key to a specific shard @@ -600,7 +588,7 @@ message GetShardIteratorRequest { // Name of the stream string stream_name = 5; // Used with shard iterator type AT_TIMESTAMP - int64 timestamp = 6 [(FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; + int64 timestamp = 6 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_DOUBLE_S_TO_INT_MS]; } message GetShardIteratorResponse { diff --git a/src/api/protos/draft/field_transformation.proto b/src/api/protos/draft/field_transformation.proto new file mode 100644 index 0000000000..d1bd16d83d --- /dev/null +++ b/src/api/protos/draft/field_transformation.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +import "google/protobuf/descriptor.proto"; + +package Ydb.FieldTransformation; + +enum EFieldTransformationType { + TRANSFORM_NONE = 0; + TRANSFORM_BASE64 = 1; + TRANSFORM_DOUBLE_S_TO_INT_MS = 2; + TRANSFORM_EMPTY_TO_NOTHING = 3; +} + +extend google.protobuf.FieldOptions { + EFieldTransformationType FieldTransformer = 58123; +} diff --git a/src/api/protos/draft/ymq.proto b/src/api/protos/draft/ymq.proto new file mode 100644 index 0000000000..ba05168fe6 --- /dev/null +++ b/src/api/protos/draft/ymq.proto @@ -0,0 +1,258 @@ +syntax = "proto3"; + +import "src/api/protos/ydb_operation.proto"; +import "src/api/protos/draft/field_transformation.proto"; + +import "google/protobuf/descriptor.proto"; + +package Ydb.Ymq.V1; +option java_package = "com.yandex.ydb.ymq.v1"; + +message ChangeMessageVisibilityRequest { + Ydb.Operations.OperationParams operation_params = 1; + string queue_url = 2; + string receipt_handle = 3; + int32 visibility_timeout = 4; +} + +message ChangeMessageVisibilityResponse { + Ydb.Operations.Operation operation = 1; +} + +message ChangeMessageVisibilityResult { +} + +message ChangeMessageVisibilityBatchRequestEntry { + string id = 1; + string receipt_handle = 2; + int32 visibility_timeout = 3; +} + +message ChangeMessageVisibilityBatchRequest { + Ydb.Operations.OperationParams operation_params = 1; + repeated ChangeMessageVisibilityBatchRequestEntry entries = 2; + string queue_url = 3; +} + +message ChangeMessageVisibilityBatchResponse { + Ydb.Operations.Operation operation = 1; +} + +message ChangeMessageVisibilityBatchResultEntry { + string id = 1; +} + +message ChangeMessageVisibilityBatchResult { + repeated BatchResultErrorEntry failed = 1; + repeated ChangeMessageVisibilityBatchResultEntry successful = 2; +} + +message CreateQueueRequest { + Ydb.Operations.OperationParams operation_params = 1; + map attributes = 2; + string queue_name = 3; + map tags = 4; +} + +message CreateQueueResponse { + Ydb.Operations.Operation operation = 1; +} + +message CreateQueueResult { + string queue_url = 1; +} + +message DeleteMessageRequest { + Ydb.Operations.OperationParams operation_params = 1; + string queue_url = 2; + string receipt_handle = 3; +} + +message DeleteMessageResponse { + Ydb.Operations.Operation operation = 1; +} + +message DeleteMessageResult { +} + +message DeleteMessageBatchRequestEntry { + string id = 1; + string receipt_handle = 2; +} + +message DeleteMessageBatchRequest { + Ydb.Operations.OperationParams operation_params = 1; + repeated DeleteMessageBatchRequestEntry entries = 2; + string queue_url = 3; +} + +message DeleteMessageBatchResponse { + Ydb.Operations.Operation operation = 1; +} + +message DeleteMessageBatchResultEntry { + string id = 1; +} + +message DeleteMessageBatchResult { + repeated BatchResultErrorEntry failed = 1; + repeated DeleteMessageBatchResultEntry successful = 2; +} + +message DeleteQueueRequest { + Ydb.Operations.OperationParams operation_params = 1; + string queue_url = 2; +} + +message DeleteQueueResponse { + Ydb.Operations.Operation operation = 1; +} + +message DeleteQueueResult { +} + +message GetQueueAttributesRequest { + Ydb.Operations.OperationParams operation_params = 1; + repeated string attribute_names = 2; + string queue_url = 3; +} + +message GetQueueAttributesResponse { + Ydb.Operations.Operation operation = 1; +} + +message GetQueueAttributesResult { + map attributes = 1; +} + +message GetQueueUrlRequest { + Ydb.Operations.OperationParams operation_params = 1; + string queue_name = 2; + string queue_owner_aws_account_id = 3; +} + +message GetQueueUrlResponse { + Ydb.Operations.Operation operation = 1; +} + +message GetQueueUrlResult { + string queue_url = 1; +} + +message ListQueuesRequest { + Ydb.Operations.OperationParams operation_params = 1; + int64 max_results = 2; + string next_token = 3; + string queue_name_prefix = 4; +} + +message ListQueuesResponse { + Ydb.Operations.Operation operation = 1; +} + +message ListQueuesResult { + string next_token = 1; + repeated string queue_urls = 2; +} + +message PurgeQueueRequest { + Ydb.Operations.OperationParams operation_params = 1; + string queue_url = 2; +} + +message PurgeQueueResponse { + Ydb.Operations.Operation operation = 1; +} + +message PurgeQueueResult { +} + +message MessageAttribute { + repeated bytes binary_list_values = 1 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_BASE64]; + bytes binary_value = 2 [(Ydb.FieldTransformation.FieldTransformer) = TRANSFORM_BASE64]; + string data_type = 3; + repeated string string_list_values = 4; + string string_value = 5; +} + +message ReceiveMessageRequest { + Ydb.Operations.OperationParams operation_params = 1; + repeated string attribute_names = 2; + int32 max_number_of_messages = 3; + repeated string message_attribute_names = 4; + repeated string message_system_attribute_names = 5; + string queue_url = 6; + string receive_request_attempt_id = 7; + int32 visibility_timeout = 8; + int32 wait_time_seconds = 9; +} + +message ReceiveMessageResponse { + Ydb.Operations.Operation operation = 1; +} + +message Message { + map attributes = 1; + string body = 2; + string md5_of_body = 3; + string md5_of_message_attributes = 4; + map message_attributes = 5; + string message_id = 6; + string receipt_handle = 7; +} + +message ReceiveMessageResult { + repeated Message messages = 1; +} + +message SendMessageRequest { + Ydb.Operations.OperationParams operation_params = 1; + int32 delay_seconds = 2; + map message_attributes = 3; + string message_body = 4; + string message_deduplication_id = 5; + string message_group_id = 6; + map message_system_attributes = 7; + string queue_url = 8; +} + +message SendMessageResponse { + Ydb.Operations.Operation operation = 1; +} + +message SendMessageResult { + string md5_of_message_attributes = 1; + string md5_of_message_body= 2; + string md5_of_message_system_attributes= 3; + string message_id = 4; + string sequence_number = 5; +} + +message SendMessageBatchRequest { + Ydb.Operations.OperationParams operation_params = 1; + repeated SendMessageRequest entries = 2; +} + +message SendMessageBatchResponse { + Ydb.Operations.Operation operation = 1; +} + +message BatchResultErrorEntry { + string code = 1; + string id = 2; + bool sender_fault = 3; + string message = 4; +} + +message SendMessageBatchResultEntry { + string md5_of_message_attributes = 1; + string md5_of_message_body= 2; + string md5_of_message_system_attributes= 3; + string message_id = 4; + string sequence_number = 5; +} + +message SendMessageBatchResult { + repeated BatchResultErrorEntry failed = 1; + repeated SendMessageBatchResultEntry successful = 2; +} diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 6e469a5884..0ff13dc8a5 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -28,3 +28,4 @@ add_subdirectory(table) add_subdirectory(topic) add_subdirectory(types) add_subdirectory(value) +add_subdirectory(ymq) diff --git a/src/client/helpers/CMakeLists.txt b/src/client/helpers/CMakeLists.txt index f9b94850ab..62e334acc4 100644 --- a/src/client/helpers/CMakeLists.txt +++ b/src/client/helpers/CMakeLists.txt @@ -1,14 +1,17 @@ _ydb_sdk_add_library(client-helpers) -target_link_libraries(client-helpers PUBLIC - yutil - client-iam-common - client-ydb_types-credentials - yql-public-issue-protos +target_link_libraries(client-helpers + PUBLIC + yutil + client-ydb_types-credentials-oauth2 + client-iam-common + client-ydb_types-credentials + yql-public-issue-protos ) -target_sources(client-helpers PRIVATE - helpers.cpp +target_sources(client-helpers + PRIVATE + helpers.cpp ) _ydb_sdk_make_client_component(Helpers client-helpers) diff --git a/src/client/helpers/helpers.cpp b/src/client/helpers/helpers.cpp index 79ebfb237e..325e04390b 100644 --- a/src/client/helpers/helpers.cpp +++ b/src/client/helpers/helpers.cpp @@ -2,8 +2,11 @@ #include #include +#include + #include #include + #include namespace NYdb { @@ -37,7 +40,7 @@ TDriverConfig CreateFromEnvironment(const std::string& connectionString) { } bool useMetadataCredentials = GetStrFromEnv("YDB_METADATA_CREDENTIALS", "0") == "1"; - if (useMetadataCredentials){ + if (useMetadataCredentials) { auto factory = CreateIamCredentialsProviderFactory(); try { factory->CreateProvider(); @@ -49,11 +52,18 @@ TDriverConfig CreateFromEnvironment(const std::string& connectionString) { } std::string accessToken = GetStrFromEnv("YDB_ACCESS_TOKEN_CREDENTIALS", ""); - if (accessToken != ""){ + if (accessToken != "") { driverConfig.SetAuthToken(accessToken); return driverConfig; } + std::string oauth2KeyFile = GetStrFromEnv("YDB_OAUTH2_KEY_FILE", ""); + if (!oauth2KeyFile.empty()) { + driverConfig.SetCredentialsProviderFactory( + CreateOauth2TokenExchangeFileCredentialsProviderFactory(oauth2KeyFile)); + return driverConfig; + } + ythrow yexception() << "Unable to create driver config from environment"; } @@ -65,4 +75,3 @@ TDriverConfig CreateFromSaKeyFile(const std::string& saKeyFile, const std::strin } } // namespace NYdb - diff --git a/src/client/types/credentials/oauth2_token_exchange/CMakeLists.txt b/src/client/types/credentials/oauth2_token_exchange/CMakeLists.txt index deec32a418..5865f03a1b 100644 --- a/src/client/types/credentials/oauth2_token_exchange/CMakeLists.txt +++ b/src/client/types/credentials/oauth2_token_exchange/CMakeLists.txt @@ -1,25 +1,30 @@ _ydb_sdk_add_library(client-ydb_types-credentials-oauth2) -target_link_libraries(client-ydb_types-credentials-oauth2 PUBLIC - yutil - jwt-cpp::jwt-cpp - cgiparam - http-misc - http-simple - json - retry - uri - client-ydb_types-credentials - client-ydb_types +target_link_libraries(client-ydb_types-credentials-oauth2 + PUBLIC + yutil + jwt-cpp::jwt-cpp + cgiparam + http-misc + http-simple + json + retry + string_utils-base64 + uri + client-ydb_types-credentials + client-ydb_types ) -target_compile_definitions(client-ydb_types-credentials-oauth2 PUBLIC - YDB_SDK_USE_NEW_JWT +target_compile_definitions(client-ydb_types-credentials-oauth2 + PUBLIC + YDB_SDK_USE_NEW_JWT ) -target_sources(client-ydb_types-credentials-oauth2 PRIVATE - credentials.cpp - jwt_token_source.cpp +target_sources(client-ydb_types-credentials-oauth2 + PRIVATE + credentials.cpp + from_file.cpp + jwt_token_source.cpp ) _ydb_sdk_install_targets(TARGETS client-ydb_types-credentials-oauth2) diff --git a/src/client/types/credentials/oauth2_token_exchange/credentials.cpp b/src/client/types/credentials/oauth2_token_exchange/credentials.cpp index 64e266be2a..a780fa8e68 100644 --- a/src/client/types/credentials/oauth2_token_exchange/credentials.cpp +++ b/src/client/types/credentials/oauth2_token_exchange/credentials.cpp @@ -141,13 +141,16 @@ struct TPrivateOauth2TokenExchangeParams: public TOauth2TokenExchangeParams { private: void ParseTokenEndpoint() { + if (TokenEndpoint_.empty()) { + throw std::invalid_argument(INV_ARG "token endpoint not set"); + } NUri::TUri url; NUri::TUri::TState::EParsed parseStatus = url.Parse(TokenEndpoint_, NUri::TFeature::FeaturesAll); if (parseStatus != NUri::TUri::TState::EParsed::ParsedOK) { throw std::invalid_argument(INV_ARG "failed to parse url"); } if (url.IsNull(NUri::TUri::FieldScheme)) { - throw std::invalid_argument(INV_ARG "token url without scheme"); + throw std::invalid_argument(TStringBuilder() << INV_ARG "token url without scheme: " << TokenEndpoint_); } TokenHost_ = TStringBuilder() << url.GetField(NUri::TUri::FieldScheme) << "://" << url.GetHost(); @@ -236,7 +239,9 @@ class TOauth2TokenExchangeProviderImpl: public std::enable_shared_from_this +#include +#include + +#include +#include + +#include +#include +#include + +#include + +namespace NYdb { + +namespace { + +struct TLessNoCase { + bool operator()(std::string_view l, std::string_view r) const { + auto ll = l.length(); + auto rl = r.length(); + if (ll != rl) { + return ll < rl; + } + return strnicmp(l.data(), r.data(), ll) < 0; + } +}; + +template +void ApplyAsymmetricAlg(TJwtTokenSourceParams* params, const std::string& privateKey) { + // Alg with first param as public key, second param as private key + params->SigningAlgorithm(std::string{}, privateKey); +} + +size_t Base64OutputLen(std::string_view input) { + while (!input.empty() && (input.back() == '=' || input.back() == ',')) { // padding + input.remove_suffix(1); + } + const size_t inputLen = input.size(); + const size_t tailEncoded = inputLen % 4; + if (tailEncoded == 1) { + throw std::runtime_error(TStringBuilder() << "invalid Base64 encoded data size: " << input.size()); + } + const size_t mainSize = (inputLen / 4) * 3; + size_t tailSize = 0; + switch (tailEncoded) { + case 2: // 12 bit => 1 byte + tailSize = 1; + break; + case 3: // 18 bits -> 2 bytes + tailSize = 2; + break; + } + return mainSize + tailSize; +} + +template +void ApplyHmacAlg(TJwtTokenSourceParams* params, const std::string& key) { + // HMAC keys are encoded in base64 encoding + const size_t base64OutputSize = Base64OutputLen(key); // throws + std::string binaryKey; + binaryKey.resize(Base64DecodeBufSize(key.size())); + // allows strings without padding + const size_t decodedBytes = Base64DecodeUneven(const_cast(binaryKey.data()), key); + if (decodedBytes != base64OutputSize) { + throw std::runtime_error("failed to decode HMAC secret from Base64"); + } + binaryKey.resize(decodedBytes); + // Alg with first param as key + params->SigningAlgorithm(binaryKey); +} + +const std::map JwtAlgorithmsFactory = { + {"RS256", &ApplyAsymmetricAlg}, + {"RS384", &ApplyAsymmetricAlg}, + {"RS512", &ApplyAsymmetricAlg}, + {"ES256", &ApplyAsymmetricAlg}, + {"ES384", &ApplyAsymmetricAlg}, + {"ES512", &ApplyAsymmetricAlg}, + {"PS256", &ApplyAsymmetricAlg}, + {"PS384", &ApplyAsymmetricAlg}, + {"PS512", &ApplyAsymmetricAlg}, + {"HS256", &ApplyHmacAlg}, + {"HS384", &ApplyHmacAlg}, + {"HS512", &ApplyHmacAlg}, +}; + +bool IsAsciiEqualUpper(const std::string& jsonParam, const std::string_view& constantInUpperCase) { + if (jsonParam.size() != constantInUpperCase.size()) { + return false; + } + for (size_t i = 0; i < constantInUpperCase.size(); ++i) { + char c = jsonParam[i]; + if (c >= 'a' && c <= 'z') { + c += 'A' - 'a'; + } + if (c != constantInUpperCase[i]) { + return false; + } + } + return true; +} + +// throws if not string +#define PROCESS_JSON_STRING_PARAM(jsonParamName, paramName, required) \ + try { \ + if (const NJson::TJsonValue* value = map.FindPtr(jsonParamName)) { \ + result.paramName(value->GetStringSafe()); \ + } else if (required) { \ + throw std::runtime_error( \ + "No \"" jsonParamName "\" parameter"); \ + } \ + } catch (const std::exception& ex) { \ + throw std::runtime_error(TStringBuilder() \ + << "Failed to parse \"" jsonParamName "\": " << ex.what()); \ + } + +// throws if not string or array of strings +#define PROCESS_JSON_ARRAY_PARAM(jsonParamName, paramName) \ + try { \ + if (const NJson::TJsonValue* value = map.FindPtr(jsonParamName)) { \ + if (value->IsArray()) { \ + for (const NJson::TJsonValue& v : value->GetArraySafe()) { \ + result.Append ## paramName(v.GetStringSafe()); \ + } \ + } else { \ + result.paramName(value->GetStringSafe()); \ + } \ + } \ + } catch (const std::exception& ex) { \ + throw std::runtime_error(TStringBuilder() \ + << "Failed to parse \"" jsonParamName "\": " << ex.what()); \ + } + +#define PROCESS_CREDS_PARAM(jsonParamName, paramName) \ + try { \ + if (const NJson::TJsonValue* value = map.FindPtr(jsonParamName)) { \ + result.paramName(ParseTokenSource(*value)); \ + } \ + } catch (const std::exception& ex) { \ + throw std::runtime_error(TStringBuilder() \ + << "Failed to parse \"" jsonParamName "\": " << ex.what()); \ + } + +// special struct to apply macros +struct TFixedTokenSourceParamsForParsing { + using TSelf = TFixedTokenSourceParamsForParsing; + + FLUENT_SETTING(std::string, Token); + FLUENT_SETTING(std::string, TokenType); +}; + +// special struct to apply macros +struct TJwtTokenSourceParamsForParsing : public TJwtTokenSourceParams { + FLUENT_SETTING(std::string, AlgStr); + FLUENT_SETTING(std::string, PrivateKeyStr); + FLUENT_SETTING(std::string, TtlStr); +}; + +std::shared_ptr ParseFixedTokenSource(const NJson::TJsonValue& cfg) { + const auto& map = cfg.GetMapSafe(); + TFixedTokenSourceParamsForParsing result; + PROCESS_JSON_STRING_PARAM("token", Token, true); // required + PROCESS_JSON_STRING_PARAM("token-type", TokenType, true); // required + return CreateFixedTokenSource(result.Token_, result.TokenType_); +} + +std::shared_ptr ParseJwtTokenSource(const NJson::TJsonValue& cfg) { + const auto& map = cfg.GetMapSafe(); + TJwtTokenSourceParamsForParsing result; + PROCESS_JSON_STRING_PARAM("kid", KeyId, false); + PROCESS_JSON_STRING_PARAM("iss", Issuer, false); + PROCESS_JSON_STRING_PARAM("sub", Subject, false); + PROCESS_JSON_ARRAY_PARAM("aud", Audience); + PROCESS_JSON_STRING_PARAM("jti", Id, false); + + // special fields + PROCESS_JSON_STRING_PARAM("ttl", TtlStr, false); + PROCESS_JSON_STRING_PARAM("alg", AlgStr, true); // required + PROCESS_JSON_STRING_PARAM("private-key", PrivateKeyStr, true); // required + + try { + if (!result.TtlStr_.empty()) { + result.TokenTtl(FromString(result.TtlStr_)); + } + } catch (const std::exception& ex) { + throw std::runtime_error(TStringBuilder() + << "Failed to parse \"ttl\": " << ex.what()); + } + + const auto jwtAlgIt = JwtAlgorithmsFactory.find(result.AlgStr_); + if (jwtAlgIt == JwtAlgorithmsFactory.end()) { + TStringBuilder err; + err << "Algorithm \"" << result.AlgStr_ << "\" is not supported. Supported algorithms are: "; + bool first = true; + for (const std::string& alg : GetSupportedOauth2TokenExchangeJwtAlgorithms()) { + if (!first) { + err << ", "; + } + first = false; + err << alg; + } + throw std::runtime_error(err); + } + jwtAlgIt->second(&result, result.PrivateKeyStr_); + return CreateJwtTokenSource(result); +} + +std::shared_ptr ParseTokenSource(const NJson::TJsonValue& cfg) { + const auto& map = cfg.GetMapSafe(); + const NJson::TJsonValue* type = map.FindPtr("type"); + if (!type) { + throw std::runtime_error("No \"type\" parameter"); + } + if (IsAsciiEqualUpper(type->GetStringSafe(), "JWT")) { + return ParseJwtTokenSource(cfg); + } else if (IsAsciiEqualUpper(type->GetStringSafe(), "FIXED")) { + return ParseFixedTokenSource(cfg); + } else { + throw std::runtime_error("Incorrect \"type\" parameter: only \"JWT\" and \"FIXED\" are supported"); + } +} + +TOauth2TokenExchangeParams ReadOauth2ConfigJson(const std::string& configJson, const std::string& tokenEndpoint) { + try { + NJson::TJsonValue cfg; + NJson::ReadJsonTree(configJson, &cfg, true); + const auto& map = cfg.GetMapSafe(); + + TOauth2TokenExchangeParams result; + if (!tokenEndpoint.empty()) { + result.TokenEndpoint(tokenEndpoint); + } else { + PROCESS_JSON_STRING_PARAM("token-endpoint", TokenEndpoint, false); // there is an explicit check in provider params after parsing + } + + PROCESS_JSON_STRING_PARAM("grant-type", GrantType, false); + PROCESS_JSON_ARRAY_PARAM("res", Resource); + PROCESS_JSON_STRING_PARAM("requested-token-type", RequestedTokenType, false); + PROCESS_JSON_ARRAY_PARAM("aud", Audience); + PROCESS_JSON_ARRAY_PARAM("scope", Scope); + PROCESS_CREDS_PARAM("subject-credentials", SubjectTokenSource); + PROCESS_CREDS_PARAM("actor-credentials", ActorTokenSource); + return result; + } catch (const std::exception& ex) { + throw std::runtime_error(TStringBuilder() << "Failed to parse config file for OAuth 2.0 token exchange: " << ex.what()); + } +} + +TOauth2TokenExchangeParams ReadOauth2ConfigFile(const std::string& configFilePath, const std::string& tokenEndpoint) { + return ReadOauth2ConfigJson(TFileInput(configFilePath).ReadAll(), tokenEndpoint); +} + +} // namespace + +std::vector GetSupportedOauth2TokenExchangeJwtAlgorithms() { + std::vector result; + result.reserve(JwtAlgorithmsFactory.size()); + for (const auto& [alg, _] : JwtAlgorithmsFactory) { + result.push_back(alg); + } + return result; +} + +std::shared_ptr CreateOauth2TokenExchangeFileCredentialsProviderFactory(const std::string& configFilePath, const std::string& tokenEndpoint) { + return CreateOauth2TokenExchangeCredentialsProviderFactory(ReadOauth2ConfigFile(configFilePath, tokenEndpoint)); +} + +} // namespace NYdb diff --git a/src/client/ymq/CMakeLists.txt b/src/client/ymq/CMakeLists.txt new file mode 100644 index 0000000000..29c0bf0441 --- /dev/null +++ b/src/client/ymq/CMakeLists.txt @@ -0,0 +1,19 @@ +_ydb_sdk_add_library(client-ymq) + +target_link_libraries(client-ymq + PUBLIC + yutil + grpc-client + string_utils-url + api-grpc-draft + library-operation_id + impl-ydb_internal-make_request + client-ydb_driver +) + +target_sources(client-ymq + PRIVATE + ymq.cpp +) + +_ydb_sdk_make_client_component(Ymq client-ymq) diff --git a/src/client/ymq/ymq.cpp b/src/client/ymq/ymq.cpp new file mode 100644 index 0000000000..10d0ca8d49 --- /dev/null +++ b/src/client/ymq/ymq.cpp @@ -0,0 +1,135 @@ +#include "ymq.h" + +#define INCLUDE_YDB_INTERNAL_H +#include +#undef INCLUDE_YDB_INTERNAL_H + +#include +#include + +#include + +namespace NYdb::Ymq::V1 { + + class TYmqClient::TImpl : public TClientImplCommon { + public: + TImpl(std::shared_ptr &&connections, const TCommonClientSettings &settings) + : TClientImplCommon(std::move(connections), settings) {} + + template + auto MakeResultExtractor(NThreading::TPromise promise) { + return [promise = std::move(promise)] + (google::protobuf::Any *any, TPlainStatus status) mutable { + std::unique_ptr result; + if (any) { + result.reset(new TProtoResult); + any->UnpackTo(result.get()); + } + + promise.SetValue( + TResultWrapper( + TStatus(std::move(status)), + std::move(result))); + }; + } + + template + NThreading::TFuture> CallImpl(const TSettings& settings, TAsyncCall grpcCall, TFillRequestFn fillRequest) { + using TResultWrapper = TProtoResultWrapper; + auto request = MakeOperationRequest(settings); + fillRequest(request); + + auto promise = NThreading::NewPromise(); + auto future = promise.GetFuture(); + + auto extractor = MakeResultExtractor(std::move(promise)); + + Connections_->RunDeferred( + std::move(request), + std::move(extractor), + grpcCall, + DbDriverState_, + INITIAL_DEFERRED_CALL_DELAY, + TRpcRequestSettings::Make(settings)); + + return future; + + } + + template + NThreading::TFuture> CallImpl(const TSettings& settings, TAsyncCall grpcCall) { + return CallImpl(settings, grpcCall, [](TProtoRequest&) {}); + } + + TAsyncGetQueueUrlResult GetQueueUrl(const std::string &queueName, TGetQueueUrlSettings settings) { + return CallImpl(settings, + &Ydb::Ymq::V1::YmqService::Stub::AsyncGetQueueUrl, + [&](Ydb::Ymq::V1::GetQueueUrlRequest& req) { + req.set_queue_name(queueName); + } + ); + } + + TAsyncCreateQueueResult CreateQueue(const std::string &queueName, TCreateQueueSettings settings) { + return CallImpl(settings, + &Ydb::Ymq::V1::YmqService::Stub::AsyncCreateQueue, + [&](Ydb::Ymq::V1::CreateQueueRequest& req) { + req.set_queue_name(queueName); + } + ); + } + + TAsyncSendMessageResult SendMessage(const std::string &queueUrl, const std::string &body, TSendMessageSettings settings) { + return CallImpl(settings, + &Ydb::Ymq::V1::YmqService::Stub::AsyncSendMessage, + [&](Ydb::Ymq::V1::SendMessageRequest& req) { + req.set_queue_url(queueUrl); + req.set_message_body(body); + } + ); + } + + template + NThreading::TFuture> DoProtoRequest(const TProtoRequest& proto, TMethod method, const TProtoRequestSettings& settings) { + return CallImpl(settings, method, + [&](TProtoRequest& req) { + req.CopyFrom(proto); + }); + } + }; + + TYmqClient::TYmqClient(const TDriver& driver, const TCommonClientSettings& settings) + : Impl_(new TImpl(CreateInternalInterface(driver), settings)) + { + } + + TAsyncGetQueueUrlResult TYmqClient::GetQueueUrl(const std::string& path, TGetQueueUrlSettings& settings) { + return Impl_->GetQueueUrl(path, settings); + } + + template + NThreading::TFuture> TYmqClient::DoProtoRequest(const TProtoRequest& request, TMethod method, TProtoRequestSettings settings) { + return Impl_->DoProtoRequest(request, method, settings); + } + + template NThreading::TFuture> TYmqClient::DoProtoRequest + < + Ydb::Ymq::V1::GetQueueUrlRequest, + Ydb::Ymq::V1::GetQueueUrlResponse, + Ydb::Ymq::V1::GetQueueUrlResult, + decltype(&Ydb::Ymq::V1::YmqService::Stub::AsyncGetQueueUrl) + >( + const Ydb::Ymq::V1::GetQueueUrlRequest& request, + decltype(&Ydb::Ymq::V1::YmqService::Stub::AsyncGetQueueUrl) method, + TProtoRequestSettings settings + ); +} diff --git a/src/client/ymq/ymq.h b/src/client/ymq/ymq.h new file mode 100644 index 0000000000..1590911d81 --- /dev/null +++ b/src/client/ymq/ymq.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include + +namespace NYdb::Ymq::V1 { + + template + class TProtoResultWrapper : public NYdb::TStatus { + friend class TYmqClient; + + private: + TProtoResultWrapper( + NYdb::TStatus&& status, + std::unique_ptr result) + : TStatus(std::move(status)) + , Result(std::move(result)) + { } + + public: + const TProtoResult& GetResult() const { + Y_ABORT_UNLESS(Result, "Uninitialized result"); + return *Result; + } + + private: + std::unique_ptr Result; + }; + + enum EStreamMode { + ESM_PROVISIONED = 1, + ESM_ON_DEMAND = 2, + }; + + using TGetQueueUrlResult = TProtoResultWrapper; + using TAsyncGetQueueUrlResult = NThreading::TFuture; + + using TCreateQueueResult = TProtoResultWrapper; + using TAsyncCreateQueueResult = NThreading::TFuture; + + using TSendMessageResult = TProtoResultWrapper; + using TAsyncSendMessageResult = NThreading::TFuture; + + struct TDataRecord { + std::string Data; + std::string PartitionKey; + std::string ExplicitHashDecimal; + }; + + struct TGetQueueUrlSettings : public NYdb::TOperationRequestSettings {}; + struct TCreateQueueSettings : public NYdb::TOperationRequestSettings {}; + struct TSendMessageSettings : public NYdb::TOperationRequestSettings {}; + struct TProtoRequestSettings : public NYdb::TOperationRequestSettings {}; + + class TYmqClient { + class TImpl; + + public: + TYmqClient(const NYdb::TDriver& driver, const NYdb::TCommonClientSettings& settings = NYdb::TCommonClientSettings()); + + TAsyncGetQueueUrlResult GetQueueUrl(const std::string& queueName, TGetQueueUrlSettings& getQueueUrlSettings); + TAsyncCreateQueueResult CreateQueue(const std::string& queueName, TCreateQueueSettings& createQueueSettings); + + template + NThreading::TFuture> DoProtoRequest(const TProtoRequest& request, TMethod method, TProtoRequestSettings settings = TProtoRequestSettings()); + + NThreading::TFuture DiscoveryCompleted(); + + private: + std::shared_ptr Impl_; + }; + +} diff --git a/tests/unit/client/oauth2_token_exchange/CMakeLists.txt b/tests/unit/client/oauth2_token_exchange/CMakeLists.txt index 02b47611f6..9fef72f137 100644 --- a/tests/unit/client/oauth2_token_exchange/CMakeLists.txt +++ b/tests/unit/client/oauth2_token_exchange/CMakeLists.txt @@ -6,6 +6,8 @@ add_ydb_test(NAME client-oauth2_ut yutil cpp-testing-unittest_main http-server + json + string_utils-base64 client-ydb_types-credentials-oauth2 LABELS unit diff --git a/tests/unit/client/oauth2_token_exchange/credentials_ut.cpp b/tests/unit/client/oauth2_token_exchange/credentials_ut.cpp index 42bd7a55ee..29a1703531 100644 --- a/tests/unit/client/oauth2_token_exchange/credentials_ut.cpp +++ b/tests/unit/client/oauth2_token_exchange/credentials_ut.cpp @@ -1,16 +1,29 @@ #include +#include +#include "jwt_check_helper.h" + +#include #include #include #include #include +#include #include #include +#include #include +#include using namespace NYdb; +extern const std::string TestRSAPrivateKeyContent; +extern const std::string TestRSAPublicKeyContent; +extern const std::string TestECPrivateKeyContent; +extern const std::string TestECPublicKeyContent; +extern const std::string TestHMACSecretKeyBase64Content; + class TTestTokenExchangeServer: public THttpServer::ICallBack { public: struct TCheck { @@ -21,11 +34,36 @@ class TTestTokenExchangeServer: public THttpServer::ICallBack { std::string Response; std::string ExpectedErrorPart; std::string Error; + std::optional SubjectJwtCheck; + std::optional ActorJwtCheck; void Check() { - UNIT_ASSERT(InputParams || !ExpectRequest); + UNIT_ASSERT_C(InputParams || !ExpectRequest, "Request error: " << Error); if (InputParams) { - UNIT_ASSERT_VALUES_EQUAL(ExpectedInputParams.Print(), InputParams->Print()); + if (SubjectJwtCheck || ActorJwtCheck) { + TCgiParameters inputParamsCopy = *InputParams; + if (SubjectJwtCheck) { + std::string subjectJwt; + UNIT_ASSERT(inputParamsCopy.Has("subject_token")); + UNIT_ASSERT(inputParamsCopy.Has("subject_token_type", "urn:ietf:params:oauth:token-type:jwt")); + subjectJwt = inputParamsCopy.Get("subject_token"); + inputParamsCopy.Erase("subject_token"); + inputParamsCopy.Erase("subject_token_type"); + SubjectJwtCheck->Check(subjectJwt); + } + if (ActorJwtCheck) { + std::string actorJwt; + UNIT_ASSERT(inputParamsCopy.Has("actor_token")); + UNIT_ASSERT(inputParamsCopy.Has("actor_token_type", "urn:ietf:params:oauth:token-type:jwt")); + actorJwt = inputParamsCopy.Get("actor_token"); + inputParamsCopy.Erase("actor_token"); + inputParamsCopy.Erase("actor_token_type"); + ActorJwtCheck->Check(actorJwt); + } + UNIT_ASSERT_VALUES_EQUAL(ExpectedInputParams.Print(), inputParamsCopy.Print()); + } else { + UNIT_ASSERT_VALUES_EQUAL(ExpectedInputParams.Print(), InputParams->Print()); + } } if (!ExpectedErrorPart.empty()) { @@ -109,6 +147,21 @@ class TTestTokenExchangeServer: public THttpServer::ICallBack { } } + void RunFromConfig(const std::string& fileName, const std::string& expectedToken = {}, bool checkExpectations = true, const std::string& explicitTokenEndpoint = {}) { + std::string token; + Run([&]() { + auto factory = CreateOauth2TokenExchangeFileCredentialsProviderFactory(fileName, explicitTokenEndpoint); + if (!expectedToken.empty()) { + token = factory->CreateProvider()->GetAuthInfo(); + } + }, + checkExpectations); + + if (!expectedToken.empty()) { + UNIT_ASSERT_VALUES_EQUAL(expectedToken, token); + } + } + void CheckExpectations() { with_lock (Lock) { Check.Check(); @@ -129,8 +182,95 @@ class TTestTokenExchangeServer: public THttpServer::ICallBack { TCheck Check; }; +template + struct TJsonFillerArray { + TJsonFillerArray(TParent* parent, NJson::TJsonWriter* writer) + : Parent(parent) + , Writer(writer) + { + Writer->OpenArray(); + } + + TJsonFillerArray& Value(const TStringBuf& value) { + Writer->Write(value); + return *this; + } + + TParent& Build() { + Writer->CloseArray(); + return *Parent; + } + + TParent* Parent = nullptr; + NJson::TJsonWriter* Writer = nullptr; + }; + + template + struct TJsonFiller { + TJsonFiller(NJson::TJsonWriter* writer) + : Writer(writer) + {} + + TDerived& Field(const TStringBuf& key, const TStringBuf& value) { + Writer->WriteKey(key); + Writer->Write(value); + return *static_cast(this); + } + + TJsonFillerArray Array(const TStringBuf& key) { + Writer->WriteKey(key); + return TJsonFillerArray(static_cast(this), Writer); + } + + NJson::TJsonWriter* Writer = nullptr; + }; + +struct TTestConfigFile : public TJsonFiller { + struct TSubMap : public TJsonFiller { + TSubMap(TTestConfigFile* parent, NJson::TJsonWriter* writer) + : TJsonFiller(writer) + , Parent(parent) + { + Writer->OpenMap(); + } + + TTestConfigFile& Build() { + Writer->CloseMap(); + return *Parent; + } + + TTestConfigFile* Parent = nullptr; + }; + + TTestConfigFile() + : TJsonFiller(&Writer) + , Writer(&Result.Out, true) + , TmpFile(MakeTempName(nullptr, "oauth2_cfg")) + { + Writer.OpenMap(); + } + + TSubMap SubMap(const TStringBuf& key) { + Writer.WriteKey(key); + return TSubMap(this, &Writer); + } + + TString Build() { + Writer.CloseMap(); + Writer.Flush(); + + TUnbufferedFileOutput(TmpFile.Name()).Write(Result); + + return TmpFile.Name(); + } + + TStringBuilder Result; + NJson::TJsonWriter Writer; + TTempFile TmpFile; +}; + Y_UNIT_TEST_SUITE(TestTokenExchange) { - Y_UNIT_TEST(Exchanges) { + void Exchanges(bool fromConfig) { TTestTokenExchangeServer server; server.Check.ExpectedInputParams.emplace("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"); server.Check.ExpectedInputParams.emplace("requested_token_type", "urn:ietf:params:oauth:token-type:access_token"); @@ -139,77 +279,197 @@ Y_UNIT_TEST_SUITE(TestTokenExchange) { server.Check.ExpectedInputParams.emplace("audience", "test_aud"); server.Check.ExpectedInputParams.emplace("scope", "s1 s2"); server.Check.Response = R"({"access_token": "hello_token", "token_type": "bEareR", "expires_in": 42})"; - server.Run( - TOauth2TokenExchangeParams() - .TokenEndpoint(server.GetEndpoint()) - .Audience("test_aud") - .AppendScope("s1") - .AppendScope("s2") - .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")), - "Bearer hello_token" - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .Field("aud", "test_aud") + .Array("scope") + .Value("s1") + .Value("s2") + .Build() + .SubMap("subject-credentials") + .Field("type", "Fixed") + .Field("token", "test_token") + .Field("token-type", "test_token_type") + .Build() + .Build(), + "Bearer hello_token" + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .Audience("test_aud") + .AppendScope("s1") + .AppendScope("s2") + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")), + "Bearer hello_token" + ); + } server.Check.ExpectedInputParams.emplace("audience", "test_aud_2"); - server.Run( - TOauth2TokenExchangeParams() - .TokenEndpoint(server.GetEndpoint()) - .AppendAudience("test_aud") - .AppendAudience("test_aud_2") - .AppendScope("s1") - .AppendScope("s2") - .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")), - "Bearer hello_token" - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .Array("aud") + .Value("test_aud") + .Value("test_aud_2") + .Build() + .Array("scope") + .Value("s1") + .Value("s2") + .Build() + .SubMap("subject-credentials") + .Field("type", "Fixed") + .Field("token", "test_token") + .Field("token-type", "test_token_type") + .Build() + .Build(), + "Bearer hello_token" + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .AppendAudience("test_aud") + .AppendAudience("test_aud_2") + .AppendScope("s1") + .AppendScope("s2") + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")), + "Bearer hello_token" + ); + } server.Check.ExpectedInputParams.erase("scope"); server.Check.ExpectedInputParams.emplace("resource", "test_res"); server.Check.ExpectedInputParams.emplace("actor_token", "act_token"); server.Check.ExpectedInputParams.emplace("actor_token_type", "act_token_type"); - server.Run( - TOauth2TokenExchangeParams() - .TokenEndpoint(server.GetEndpoint()) - .AppendAudience("test_aud") - .AppendAudience("test_aud_2") - .Resource("test_res") - .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")) - .ActorTokenSource(CreateFixedTokenSource("act_token", "act_token_type")), - "Bearer hello_token" - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .Array("aud") + .Value("test_aud") + .Value("test_aud_2") + .Build() + .Field("res", "test_res") + .SubMap("subject-credentials") + .Field("type", "Fixed") + .Field("token", "test_token") + .Field("token-type", "test_token_type") + .Build() + .SubMap("actor-credentials") + .Field("type", "Fixed") + .Field("token", "act_token") + .Field("token-type", "act_token_type") + .Build() + .Build(), + "Bearer hello_token" + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .AppendAudience("test_aud") + .AppendAudience("test_aud_2") + .Resource("test_res") + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")) + .ActorTokenSource(CreateFixedTokenSource("act_token", "act_token_type")), + "Bearer hello_token" + ); + } } - Y_UNIT_TEST(BadParams) { + Y_UNIT_TEST(Exchanges) { + Exchanges(false); + } + + Y_UNIT_TEST(ExchangesFromConfig) { + Exchanges(true); + } + + void BadParams(bool fromConfig) { TTestTokenExchangeServer server; server.Check.ExpectRequest = false; server.Check.ExpectedErrorPart = "no token endpoint"; - server.Run( - TOauth2TokenExchangeParams() - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Build() + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + ); + } server.Check.ExpectedErrorPart = "empty audience"; - server.Run( - TOauth2TokenExchangeParams() - .TokenEndpoint(server.GetEndpoint()) - .AppendAudience("a") - .AppendAudience("") - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .Array("aud") + .Value("a") + .Value("") + .Build() + .Build() + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .AppendAudience("a") + .AppendAudience("") + ); + } server.Check.ExpectedErrorPart = "empty scope"; - server.Run( - TOauth2TokenExchangeParams() - .TokenEndpoint(server.GetEndpoint()) - .AppendScope("s") - .AppendScope("") - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .Array("scope") + .Value("s") + .Value("") + .Build() + .Build() + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .AppendScope("s") + .AppendScope("") + ); + } server.Check.ExpectedErrorPart = "failed to parse url"; - server.Run( - TOauth2TokenExchangeParams() - .TokenEndpoint("not an url") - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", "not an url") + .Build() + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint("not an url") + ); + } } - Y_UNIT_TEST(BadResponse) { + Y_UNIT_TEST(BadParams) { + BadParams(false); + } + + Y_UNIT_TEST(BadParamsFromConfig) { + BadParams(true); + } + + void BadResponse(bool fromConfig) { TTestTokenExchangeServer server; server.Check.ExpectedInputParams.emplace("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"); server.Check.ExpectedInputParams.emplace("requested_token_type", "urn:ietf:params:oauth:token-type:access_token"); @@ -217,92 +477,147 @@ Y_UNIT_TEST_SUITE(TestTokenExchange) { server.Check.ExpectedInputParams.emplace("subject_token_type", "test_token_type"); TOauth2TokenExchangeParams params; - params - .TokenEndpoint(server.GetEndpoint()) - .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")); + TTestConfigFile cfg; + if (fromConfig) { + cfg + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "Fixed") + .Field("token", "test_token") + .Field("token-type", "test_token_type") + .Build() + .Build(); + } else { + params + .TokenEndpoint(server.GetEndpoint()) + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")); + } + + auto run = [&]() { + if (fromConfig) { + server.RunFromConfig(cfg.TmpFile.Name()); + } else { + server.Run(params); + } + }; server.Check.Response = R"(})"; server.Check.ExpectedErrorPart = "json parsing error"; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "hello_token", "token_type1": "bearer", "expires_in": 42})"; server.Check.ExpectedErrorPart = "no field \"token_type\" in response"; - server.Run(params); + run(); server.Check.Response = R"({"access_token1": "hello_token", "token_type": "bearer", "expires_in": 42})"; server.Check.ExpectedErrorPart = "no field \"access_token\" in response"; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "hello_token", "token_type": "bearer", "expires_in1": 42})"; server.Check.ExpectedErrorPart = "no field \"expires_in\" in response"; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "hello_token", "token_type": "abc", "expires_in": 42})"; server.Check.ExpectedErrorPart = "unsupported token type: \"abc\""; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "hello_token", "token_type": "bearer", "expires_in": 0})"; server.Check.ExpectedErrorPart = "incorrect expiration time: 0"; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "hello_token", "token_type": "bearer", "expires_in": "hello"})"; server.Check.ExpectedErrorPart = "incorrect expiration time: 0"; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "hello_token", "token_type": "bearer", "expires_in": -1})"; server.Check.ExpectedErrorPart = "incorrect expiration time: -1"; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "hello_token", "token_type": "bearer", "expires_in": 1, "scope": "s"})"; server.Check.ExpectedErrorPart = "different scope. Expected \"\", but got \"s\""; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "", "token_type": "bearer", "expires_in": 1})"; server.Check.ExpectedErrorPart = "got empty token"; - server.Run(params); + run(); server.Check.Response = R"({"access_token": "hello_token", "token_type": "bearer", "expires_in": 1, "scope": "s a"})"; server.Check.ExpectedErrorPart = "different scope. Expected \"a\", but got \"s a\""; server.Check.ExpectedInputParams.emplace("scope", "a"); - server.Run( - TOauth2TokenExchangeParams() - .TokenEndpoint(server.GetEndpoint()) - .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")) - .Scope("a") - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "Fixed") + .Field("token", "test_token") + .Field("token-type", "test_token_type") + .Build() + .Field("scope", "a") + .Build() + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")) + .Scope("a") + ); + } server.Check.ExpectedInputParams.erase("scope"); server.Check.ExpectedErrorPart = "can not connect to"; server.Check.ExpectRequest = false; - server.Run( - TOauth2TokenExchangeParams() - .TokenEndpoint("https://localhost:42/aaa") - .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")) - ); + if (fromConfig) { + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", "https://localhost:42/aaa") + .SubMap("subject-credentials") + .Field("type", "Fixed") + .Field("token", "test_token") + .Field("token-type", "test_token_type") + .Build() + .Build() + ); + } else { + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint("https://localhost:42/aaa") + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")) + ); + } server.Check.ExpectRequest = true; // parsing response server.Check.StatusCode = HTTP_FORBIDDEN; server.Check.Response = R"(not json)"; server.Check.ExpectedErrorPart = "Exchange token error in Oauth 2 token exchange credentials provider: 403 Forbidden, could not parse response: "; - server.Run(params); + run(); server.Check.Response = R"({"error": "smth"})"; server.Check.ExpectedErrorPart = "Exchange token error in Oauth 2 token exchange credentials provider: 403 Forbidden, error: smth"; - server.Run(params); + run(); server.Check.Response = R"({"error": "smth", "error_description": "something terrible happened"})"; server.Check.ExpectedErrorPart = "Exchange token error in Oauth 2 token exchange credentials provider: 403 Forbidden, error: smth, description: something terrible happened"; - server.Run(params); + run(); server.Check.StatusCode = HTTP_BAD_REQUEST; server.Check.Response = R"({"error_uri": "my_uri", "error_description": "something terrible happened"})"; server.Check.ExpectedErrorPart = "Exchange token error in Oauth 2 token exchange credentials provider: 400 Bad request, description: something terrible happened, error_uri: my_uri"; - server.Run(params); + run(); } - Y_UNIT_TEST(UpdatesToken) { + Y_UNIT_TEST(BadResponse) { + BadResponse(false); + } + + Y_UNIT_TEST(BadResponseFromConfig) { + BadResponse(true); + } + + void UpdatesToken(bool fromConfig) { TCredentialsProviderFactoryPtr factory; TTestTokenExchangeServer server; @@ -313,10 +628,22 @@ Y_UNIT_TEST_SUITE(TestTokenExchange) { server.Check.Response = R"({"access_token": "token_1", "token_type": "bearer", "expires_in": 1})"; server.Run( [&]() { - factory = CreateOauth2TokenExchangeCredentialsProviderFactory( - TOauth2TokenExchangeParams() - .TokenEndpoint(server.GetEndpoint()) - .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type"))); + if (fromConfig) { + factory = CreateOauth2TokenExchangeFileCredentialsProviderFactory( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "Fixed") + .Field("token", "test_token") + .Field("token-type", "test_token_type") + .Build() + .Build()); + } else { + factory = CreateOauth2TokenExchangeCredentialsProviderFactory( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type"))); + } UNIT_ASSERT_VALUES_EQUAL(factory->CreateProvider()->GetAuthInfo(), "Bearer token_1"); } ); @@ -335,6 +662,14 @@ Y_UNIT_TEST_SUITE(TestTokenExchange) { ); } + Y_UNIT_TEST(UpdatesToken) { + UpdatesToken(false); + } + + Y_UNIT_TEST(UpdatesTokenFromConfig) { + UpdatesToken(true); + } + Y_UNIT_TEST(UsesCachedToken) { TCredentialsProviderFactoryPtr factory; TInstant startTime; @@ -561,4 +896,346 @@ Y_UNIT_TEST_SUITE(TestTokenExchange) { const TInstant shutdownStop = TInstant::Now(); Cerr << "Shutdown: " << (shutdownStop - shutdownStart) << Endl; } + + Y_UNIT_TEST(ExchangesFromFileConfig) { + TTestTokenExchangeServer server; + server.Check.ExpectedInputParams.emplace("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"); + server.Check.ExpectedInputParams.emplace("requested_token_type", "urn:ietf:params:oauth:token-type:access_token"); + server.Check.ExpectedInputParams.emplace("subject_token", "test_token"); + server.Check.ExpectedInputParams.emplace("subject_token_type", "test_token_type"); + server.Check.ExpectedInputParams.emplace("audience", "test_aud"); + server.Check.ExpectedInputParams.emplace("scope", "s1 s2"); + server.Check.Response = R"({"access_token": "hello_token", "token_type": "bEareR", "expires_in": 42})"; + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .Audience("test_aud") + .AppendScope("s1") + .AppendScope("s2") + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")), + "Bearer hello_token" + ); + + server.Check.ExpectedInputParams.emplace("audience", "test_aud_2"); + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .AppendAudience("test_aud") + .AppendAudience("test_aud_2") + .AppendScope("s1") + .AppendScope("s2") + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")), + "Bearer hello_token" + ); + + server.Check.ExpectedInputParams.erase("scope"); + server.Check.ExpectedInputParams.emplace("resource", "test_res"); + server.Check.ExpectedInputParams.emplace("resource", "test_res_2"); + server.Check.ExpectedInputParams.emplace("actor_token", "act_token"); + server.Check.ExpectedInputParams.emplace("actor_token_type", "act_token_type"); + server.Run( + TOauth2TokenExchangeParams() + .TokenEndpoint(server.GetEndpoint()) + .AppendAudience("test_aud") + .AppendAudience("test_aud_2") + .AppendResource("test_res") + .AppendResource("test_res_2") + .SubjectTokenSource(CreateFixedTokenSource("test_token", "test_token_type")) + .ActorTokenSource(CreateFixedTokenSource("act_token", "act_token_type")), + "Bearer hello_token" + ); + } + + Y_UNIT_TEST(SkipsUnknownFieldsInConfig) { + TTestTokenExchangeServer server; + server.Check.ExpectedInputParams.emplace("grant_type", "test_grant_type"); + server.Check.ExpectedInputParams.emplace("requested_token_type", "test_requested_token_type"); + server.Check.ExpectedInputParams.emplace("subject_token", "test_token"); + server.Check.ExpectedInputParams.emplace("subject_token_type", "test_token_type"); + server.Check.ExpectedInputParams.emplace("resource", "r1"); + server.Check.ExpectedInputParams.emplace("resource", "r2"); + server.Check.Response = R"({"access_token": "received_token", "token_type": "bEareR", "expires_in": 42})"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", "bla-bla-bla") // use explicit endpoint via param + .Field("unknown", "unknown value") + .Field("grant-type", "test_grant_type") + .Field("requested-token-type", "test_requested_token_type") + .Array("res") + .Value("r1") + .Value("r2") + .Build() + .SubMap("subject-credentials") + .Field("type", "Fixed") + .Field("token", "test_token") + .Field("token-type", "test_token_type") + .Field("unknown", "unknown value") + .Build() + .Build(), + "Bearer received_token", + true, + server.GetEndpoint() + ); + } + + Y_UNIT_TEST(JwtTokenSourceInConfig) { + TTestTokenExchangeServer server; + server.Check.ExpectedInputParams.emplace("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"); + server.Check.ExpectedInputParams.emplace("requested_token_type", "urn:ietf:params:oauth:token-type:access_token"); + server.Check.SubjectJwtCheck.emplace() + .TokenTtl(TDuration::Hours(24)) + .KeyId("test_key_id") + .Issuer("test_iss") + .Subject("test_sub") + .Audience("test_aud") + .Id("test_jti") + .Alg(TestRSAPublicKeyContent); + server.Check.ActorJwtCheck.emplace() + .AppendAudience("a1") + .AppendAudience("a2"); + server.Check.Response = R"({"access_token": "received_token", "token_type": "bEareR", "expires_in": 42})"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("ttl", "24h") + .Field("kid", "test_key_id") + .Field("iss", "test_iss") + .Field("sub", "test_sub") + .Field("aud", "test_aud") + .Field("jti", "test_jti") + .Field("alg", "rs384") + .Field("private-key", TestRSAPrivateKeyContent) + .Field("unknown", "unknown value") + .Build() + .SubMap("actor-credentials") + .Field("type", "JWT") + .Array("aud") + .Value("a1") + .Value("a2") + .Build() + .Field("alg", "RS256") + .Field("private-key", TestRSAPrivateKeyContent) + .Build() + .Build(), + "Bearer received_token" + ); + + // Other signing methods + server.Check.SubjectJwtCheck.emplace() + .Id("jti") + .Alg(Base64Decode(TestHMACSecretKeyBase64Content)); + server.Check.ActorJwtCheck.emplace() + .Alg(TestECPublicKeyContent) + .Issuer("iss"); + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("jti", "jti") + .Field("alg", "HS384") + .Field("private-key", TestHMACSecretKeyBase64Content) + .Build() + .SubMap("actor-credentials") + .Field("type", "JWT") + .Field("alg", "ES256") + .Field("private-key", TestECPrivateKeyContent) + .Field("iss", "iss") + .Build() + .Build(), + "Bearer received_token" + ); + } + + Y_UNIT_TEST(BadConfigParams) { + TTestTokenExchangeServer server; + server.Check.ExpectRequest = false; + + // wrong format + TTempFile cfg(MakeTempName(nullptr, "oauth2_cfg")); + TUnbufferedFileOutput(cfg.Name()).Write(""); // empty + server.Check.ExpectedErrorPart = "Failed to parse config file"; + server.RunFromConfig(cfg.Name()); + + TUnbufferedFileOutput(cfg.Name()).Write("not a json"); + server.Check.ExpectedErrorPart = "Failed to parse config file"; + server.RunFromConfig(cfg.Name()); + + TUnbufferedFileOutput(cfg.Name()).Write("[\"not a map\"]"); + server.Check.ExpectedErrorPart = "Not a map"; + server.RunFromConfig(cfg.Name()); + + server.Check.ExpectedErrorPart = "No \"type\" parameter"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type1", "unknown") + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "Incorrect \"type\" parameter"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "unknown") + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "Failed to parse \"iss\""; // must be string + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Array("iss") + .Value("1") + .Build() + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "No \"token\" parameter"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "fixed") + .Field("token-type", "test_token_type") + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "No \"token-type\" parameter"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "fixed") + .Field("token", "test_token") + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "No \"alg\" parameter"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("private-key", TestRSAPrivateKeyContent) + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "No \"private-key\" parameter"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("alg", "rs256") + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "Failed to parse \"ttl\""; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("alg", "rs256") + .Field("private-key", TestRSAPrivateKeyContent) + .Field("ttl", "-1s") + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "Algorithm \"algorithm\" is not supported. Supported algorithms are: "; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("alg", "algorithm") + .Field("private-key", TestRSAPrivateKeyContent) + .Build() + .Build() + ); +#ifdef YDB_SDK_USE_NEW_JWT + server.Check.ExpectedErrorPart = "failed to load key"; +#else + server.Check.ExpectedErrorPart = "failed to load private key"; +#endif + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("alg", "rs256") + .Field("private-key", "not a key") + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "failed to decode HMAC secret from Base64"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("alg", "hs256") + .Field("private-key", "\n\n") + .Build() + .Build() + ); + +#ifdef YDB_SDK_USE_NEW_JWT + server.Check.ExpectedErrorPart = "invalid key size"; +#else + server.Check.ExpectedErrorPart = "failed to load private key"; +#endif + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("alg", "es256") + .Field("private-key", TestRSAPrivateKeyContent) // Need EC key + .Build() + .Build() + ); + +#ifdef YDB_SDK_USE_NEW_JWT + server.Check.ExpectedErrorPart = "failed to load key"; +#else + server.Check.ExpectedErrorPart = "failed to load private key"; +#endif + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .SubMap("subject-credentials") + .Field("type", "jwt") + .Field("alg", "ps512") + .Field("private-key", TestHMACSecretKeyBase64Content) // Need RSA key + .Build() + .Build() + ); + + server.Check.ExpectedErrorPart = "Not a map"; + server.RunFromConfig( + TTestConfigFile() + .Field("token-endpoint", server.GetEndpoint()) + .Array("subject-credentials") + .Value("42") + .Build() + .Build() + ); + } } diff --git a/tests/unit/client/oauth2_token_exchange/jwt_check_helper.h b/tests/unit/client/oauth2_token_exchange/jwt_check_helper.h new file mode 100644 index 0000000000..3b23e2fbe2 --- /dev/null +++ b/tests/unit/client/oauth2_token_exchange/jwt_check_helper.h @@ -0,0 +1,98 @@ +#include +#include + +#include + +extern const std::string TestRSAPrivateKeyContent; +extern const std::string TestRSAPublicKeyContent; + +struct TJwtCheck { + using TSelf = TJwtCheck; +#ifdef YDB_SDK_USE_NEW_JWT + using TDecodedJwt = jwt::decoded_jwt; +#else + using TDecodedJwt = jwt::decoded_jwt; +#endif + + struct IAlgCheck { + virtual void Check(const TDecodedJwt& decoded) const = 0; + virtual ~IAlgCheck() = default; + }; + + template + struct TAlgCheck : public IAlgCheck { + TAlgCheck(const std::string& publicKey) + : Alg(publicKey) + {} + + void Check(const TDecodedJwt& decoded) const override { + UNIT_ASSERT_VALUES_EQUAL(decoded.get_algorithm(), Alg.name()); + const std::string data = decoded.get_header_base64() + "." + decoded.get_payload_base64(); + const std::string signature = decoded.get_signature(); +#ifdef YDB_SDK_USE_NEW_JWT + std::error_code ec; + Alg.verify(data, signature, ec); + if (ec) { + throw std::runtime_error(ec.message()); + } +#else + Alg.verify(data, signature); // Throws +#endif + } + + TAlg Alg; + }; + + template + TSelf& Alg(const std::string& publicKey) { + Alg_.reset(new TAlgCheck(publicKey)); + return *this; + } + std::unique_ptr Alg_ = std::make_unique>(TestRSAPublicKeyContent); + + FLUENT_SETTING_OPTIONAL(std::string, KeyId); + + FLUENT_SETTING_OPTIONAL(std::string, Issuer); + FLUENT_SETTING_OPTIONAL(std::string, Subject); + FLUENT_SETTING_OPTIONAL(std::string, Id); + FLUENT_SETTING_VECTOR_OR_SINGLE(std::string, Audience); + FLUENT_SETTING_DEFAULT(TDuration, TokenTtl, TDuration::Hours(1)); + + void Check(const std::string& token) { + TDecodedJwt decoded(token); + + UNIT_ASSERT_VALUES_EQUAL(decoded.get_type(), "JWT"); + Alg_->Check(decoded); + + const auto now = std::chrono::system_clock::from_time_t(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); + UNIT_ASSERT(decoded.get_issued_at() >= now - std::chrono::minutes(10)); + UNIT_ASSERT(decoded.get_issued_at() <= now); + UNIT_ASSERT_EQUAL(decoded.get_expires_at() - decoded.get_issued_at(), std::chrono::seconds(TokenTtl_.Seconds())); + +#define CHECK_JWT_CLAIM(claim, option) \ + if (option) { \ + UNIT_ASSERT(decoded.has_ ## claim()); \ + UNIT_ASSERT_VALUES_EQUAL(decoded.get_ ## claim(), *option); \ + } else { \ + UNIT_ASSERT(!decoded.has_ ## claim()); \ + } + + CHECK_JWT_CLAIM(key_id, KeyId_); + CHECK_JWT_CLAIM(issuer, Issuer_); + CHECK_JWT_CLAIM(subject, Subject_); + CHECK_JWT_CLAIM(id, Id_); + +#undef CHECK_JWT_CLAIM + + if (!Audience_.empty()) { + UNIT_ASSERT(decoded.has_audience()); + const std::set aud = decoded.get_audience(); + UNIT_ASSERT_VALUES_EQUAL(aud.size(), Audience_.size()); + for (const std::string& a : Audience_) { + UNIT_ASSERT(aud.contains(a)); + } + } else { + UNIT_ASSERT(!decoded.has_audience()); + } + } +}; diff --git a/tests/unit/client/oauth2_token_exchange/jwt_token_source_ut.cpp b/tests/unit/client/oauth2_token_exchange/jwt_token_source_ut.cpp index db07fe55b6..ddc9ecb05c 100644 --- a/tests/unit/client/oauth2_token_exchange/jwt_token_source_ut.cpp +++ b/tests/unit/client/oauth2_token_exchange/jwt_token_source_ut.cpp @@ -1,4 +1,5 @@ #include +#include "jwt_check_helper.h" #include @@ -6,46 +7,33 @@ using namespace NYdb; -const std::string TestPrivateKeyContent = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC75/JS3rMcLJxv\nFgpOzF5+2gH+Yig3RE2MTl9uwC0BZKAv6foYr7xywQyWIK+W1cBhz8R4LfFmZo2j\nM0aCvdRmNBdW0EDSTnHLxCsFhoQWLVq+bI5f5jzkcoiioUtaEpADPqwgVULVtN/n\nnPJiZ6/dU30C3jmR6+LUgEntUtWt3eq3xQIn5lG3zC1klBY/HxtfH5Hu8xBvwRQT\nJnh3UpPLj8XwSmriDgdrhR7o6umWyVuGrMKlLHmeivlfzjYtfzO1MOIMG8t2/zxG\nR+xb4Vwks73sH1KruH/0/JMXU97npwpe+Um+uXhpldPygGErEia7abyZB2gMpXqr\nWYKMo02NAgMBAAECggEAO0BpC5OYw/4XN/optu4/r91bupTGHKNHlsIR2rDzoBhU\nYLd1evpTQJY6O07EP5pYZx9mUwUdtU4KRJeDGO/1/WJYp7HUdtxwirHpZP0lQn77\nuccuX/QQaHLrPekBgz4ONk+5ZBqukAfQgM7fKYOLk41jgpeDbM2Ggb6QUSsJISEp\nzrwpI/nNT/wn+Hvx4DxrzWU6wF+P8kl77UwPYlTA7GsT+T7eKGVH8xsxmK8pt6lg\nsvlBA5XosWBWUCGLgcBkAY5e4ZWbkdd183o+oMo78id6C+PQPE66PLDtHWfpRRmN\nm6XC03x6NVhnfvfozoWnmS4+e4qj4F/emCHvn0GMywKBgQDLXlj7YPFVXxZpUvg/\nrheVcCTGbNmQJ+4cZXx87huqwqKgkmtOyeWsRc7zYInYgraDrtCuDBCfP//ZzOh0\nLxepYLTPk5eNn/GT+VVrqsy35Ccr60g7Lp/bzb1WxyhcLbo0KX7/6jl0lP+VKtdv\nmto+4mbSBXSM1Y5BVVoVgJ3T/wKBgQDsiSvPRzVi5TTj13x67PFymTMx3HCe2WzH\nJUyepCmVhTm482zW95pv6raDr5CTO6OYpHtc5sTTRhVYEZoEYFTM9Vw8faBtluWG\nBjkRh4cIpoIARMn74YZKj0C/0vdX7SHdyBOU3bgRPHg08Hwu3xReqT1kEPSI/B2V\n4pe5fVrucwKBgQCNFgUxUA3dJjyMES18MDDYUZaRug4tfiYouRdmLGIxUxozv6CG\nZnbZzwxFt+GpvPUV4f+P33rgoCvFU+yoPctyjE6j+0aW0DFucPmb2kBwCu5J/856\nkFwCx3blbwFHAco+SdN7g2kcwgmV2MTg/lMOcU7XwUUcN0Obe7UlWbckzQKBgQDQ\nnXaXHL24GGFaZe4y2JFmujmNy1dEsoye44W9ERpf9h1fwsoGmmCKPp90az5+rIXw\nFXl8CUgk8lXW08db/r4r+ma8Lyx0GzcZyplAnaB5/6j+pazjSxfO4KOBy4Y89Tb+\nTP0AOcCi6ws13bgY+sUTa/5qKA4UVw+c5zlb7nRpgwKBgGXAXhenFw1666482iiN\ncHSgwc4ZHa1oL6aNJR1XWH+aboBSwR+feKHUPeT4jHgzRGo/aCNHD2FE5I8eBv33\nof1kWYjAO0YdzeKrW0rTwfvt9gGg+CS397aWu4cy+mTI+MNfBgeDAIVBeJOJXLlX\nhL8bFAuNNVrCOp79TNnNIsh7\n-----END PRIVATE KEY-----\n"; -const std::string TestPublicKeyContent = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+fyUt6zHCycbxYKTsxe\nftoB/mIoN0RNjE5fbsAtAWSgL+n6GK+8csEMliCvltXAYc/EeC3xZmaNozNGgr3U\nZjQXVtBA0k5xy8QrBYaEFi1avmyOX+Y85HKIoqFLWhKQAz6sIFVC1bTf55zyYmev\n3VN9At45kevi1IBJ7VLVrd3qt8UCJ+ZRt8wtZJQWPx8bXx+R7vMQb8EUEyZ4d1KT\ny4/F8Epq4g4Ha4Ue6OrplslbhqzCpSx5nor5X842LX8ztTDiDBvLdv88RkfsW+Fc\nJLO97B9Sq7h/9PyTF1Pe56cKXvlJvrl4aZXT8oBhKxImu2m8mQdoDKV6q1mCjKNN\njQIDAQAB\n-----END PUBLIC KEY-----\n"; +extern const std::string TestRSAPrivateKeyContent = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC75/JS3rMcLJxv\nFgpOzF5+2gH+Yig3RE2MTl9uwC0BZKAv6foYr7xywQyWIK+W1cBhz8R4LfFmZo2j\nM0aCvdRmNBdW0EDSTnHLxCsFhoQWLVq+bI5f5jzkcoiioUtaEpADPqwgVULVtN/n\nnPJiZ6/dU30C3jmR6+LUgEntUtWt3eq3xQIn5lG3zC1klBY/HxtfH5Hu8xBvwRQT\nJnh3UpPLj8XwSmriDgdrhR7o6umWyVuGrMKlLHmeivlfzjYtfzO1MOIMG8t2/zxG\nR+xb4Vwks73sH1KruH/0/JMXU97npwpe+Um+uXhpldPygGErEia7abyZB2gMpXqr\nWYKMo02NAgMBAAECggEAO0BpC5OYw/4XN/optu4/r91bupTGHKNHlsIR2rDzoBhU\nYLd1evpTQJY6O07EP5pYZx9mUwUdtU4KRJeDGO/1/WJYp7HUdtxwirHpZP0lQn77\nuccuX/QQaHLrPekBgz4ONk+5ZBqukAfQgM7fKYOLk41jgpeDbM2Ggb6QUSsJISEp\nzrwpI/nNT/wn+Hvx4DxrzWU6wF+P8kl77UwPYlTA7GsT+T7eKGVH8xsxmK8pt6lg\nsvlBA5XosWBWUCGLgcBkAY5e4ZWbkdd183o+oMo78id6C+PQPE66PLDtHWfpRRmN\nm6XC03x6NVhnfvfozoWnmS4+e4qj4F/emCHvn0GMywKBgQDLXlj7YPFVXxZpUvg/\nrheVcCTGbNmQJ+4cZXx87huqwqKgkmtOyeWsRc7zYInYgraDrtCuDBCfP//ZzOh0\nLxepYLTPk5eNn/GT+VVrqsy35Ccr60g7Lp/bzb1WxyhcLbo0KX7/6jl0lP+VKtdv\nmto+4mbSBXSM1Y5BVVoVgJ3T/wKBgQDsiSvPRzVi5TTj13x67PFymTMx3HCe2WzH\nJUyepCmVhTm482zW95pv6raDr5CTO6OYpHtc5sTTRhVYEZoEYFTM9Vw8faBtluWG\nBjkRh4cIpoIARMn74YZKj0C/0vdX7SHdyBOU3bgRPHg08Hwu3xReqT1kEPSI/B2V\n4pe5fVrucwKBgQCNFgUxUA3dJjyMES18MDDYUZaRug4tfiYouRdmLGIxUxozv6CG\nZnbZzwxFt+GpvPUV4f+P33rgoCvFU+yoPctyjE6j+0aW0DFucPmb2kBwCu5J/856\nkFwCx3blbwFHAco+SdN7g2kcwgmV2MTg/lMOcU7XwUUcN0Obe7UlWbckzQKBgQDQ\nnXaXHL24GGFaZe4y2JFmujmNy1dEsoye44W9ERpf9h1fwsoGmmCKPp90az5+rIXw\nFXl8CUgk8lXW08db/r4r+ma8Lyx0GzcZyplAnaB5/6j+pazjSxfO4KOBy4Y89Tb+\nTP0AOcCi6ws13bgY+sUTa/5qKA4UVw+c5zlb7nRpgwKBgGXAXhenFw1666482iiN\ncHSgwc4ZHa1oL6aNJR1XWH+aboBSwR+feKHUPeT4jHgzRGo/aCNHD2FE5I8eBv33\nof1kWYjAO0YdzeKrW0rTwfvt9gGg+CS397aWu4cy+mTI+MNfBgeDAIVBeJOJXLlX\nhL8bFAuNNVrCOp79TNnNIsh7\n-----END PRIVATE KEY-----\n"; +extern const std::string TestRSAPublicKeyContent = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu+fyUt6zHCycbxYKTsxe\nftoB/mIoN0RNjE5fbsAtAWSgL+n6GK+8csEMliCvltXAYc/EeC3xZmaNozNGgr3U\nZjQXVtBA0k5xy8QrBYaEFi1avmyOX+Y85HKIoqFLWhKQAz6sIFVC1bTf55zyYmev\n3VN9At45kevi1IBJ7VLVrd3qt8UCJ+ZRt8wtZJQWPx8bXx+R7vMQb8EUEyZ4d1KT\ny4/F8Epq4g4Ha4Ue6OrplslbhqzCpSx5nor5X842LX8ztTDiDBvLdv88RkfsW+Fc\nJLO97B9Sq7h/9PyTF1Pe56cKXvlJvrl4aZXT8oBhKxImu2m8mQdoDKV6q1mCjKNN\njQIDAQAB\n-----END PUBLIC KEY-----\n"; +extern const std::string TestECPrivateKeyContent = "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIB6fv25gf7P/7fkjW/2kcKICUhHeOygkFeUJ/ylyU3hloAoGCCqGSM49\nAwEHoUQDQgAEvkKy92hpLiT0GEpzFkYBEWWnkAGTTA6141H0oInA9X30eS0RObAa\nmVY8yD39NI7Nj03hBxEa4Z0tOhrq9cW8eg==\n-----END EC PRIVATE KEY-----\n"; +extern const std::string TestECPublicKeyContent = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvkKy92hpLiT0GEpzFkYBEWWnkAGT\nTA6141H0oInA9X30eS0RObAamVY8yD39NI7Nj03hBxEa4Z0tOhrq9cW8eg==\n-----END PUBLIC KEY-----\n"; +extern const std::string TestHMACSecretKeyBase64Content = "VGhlIHdvcmxkIGhhcyBjaGFuZ2VkLgpJIHNlZSBpdCBpbiB0aGUgd2F0ZXIuCkkgZmVlbCBpdCBpbiB0aGUgRWFydGguCkkgc21lbGwgaXQgaW4gdGhlIGFpci4KTXVjaCB0aGF0IG9uY2Ugd2FzIGlzIGxvc3QsCkZvciBub25lIG5vdyBsaXZlIHdobyByZW1lbWJlciBpdC4K"; Y_UNIT_TEST_SUITE(JwtTokenSourceTest) { Y_UNIT_TEST(Encodes) { auto source = CreateJwtTokenSource( TJwtTokenSourceParams() .KeyId("test_key_id") - .SigningAlgorithm("", TestPrivateKeyContent) + .SigningAlgorithm("", TestRSAPrivateKeyContent) .Issuer("test_issuer") .Subject("test_subject") .Audience("test_audience") ); - const auto now1 = std::chrono::system_clock::from_time_t(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); - auto t = source->GetToken(); - const auto now2 = std::chrono::system_clock::from_time_t(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())); - UNIT_ASSERT_VALUES_EQUAL(t.TokenType, "urn:ietf:params:oauth:token-type:jwt"); - jwt::decoded_jwt decoded(t.Token); - UNIT_ASSERT_VALUES_EQUAL(decoded.get_type(), "JWT"); - UNIT_ASSERT_VALUES_EQUAL(decoded.get_algorithm(), "RS256"); - UNIT_ASSERT_VALUES_EQUAL(decoded.get_key_id(), "test_key_id"); - UNIT_ASSERT(!decoded.has_id()); - UNIT_ASSERT_VALUES_EQUAL(decoded.get_issuer(), "test_issuer"); - UNIT_ASSERT_VALUES_EQUAL(decoded.get_subject(), "test_subject"); - UNIT_ASSERT_VALUES_EQUAL(decoded.get_audience().size(), 1); - UNIT_ASSERT_VALUES_EQUAL(*decoded.get_audience().begin(), "test_audience"); - UNIT_ASSERT(decoded.get_issued_at() >= now1); - UNIT_ASSERT(decoded.get_issued_at() <= now2); - UNIT_ASSERT(decoded.get_expires_at() >= now1 + std::chrono::hours(1)); - UNIT_ASSERT(decoded.get_expires_at() <= now2 + std::chrono::hours(1)); + TJwtCheck check; + check + .KeyId("test_key_id") + .Issuer("test_issuer") + .Audience("test_audience") + .Subject("test_subject"); - const std::string data = decoded.get_header_base64() + "." + decoded.get_payload_base64(); - const std::string signature = decoded.get_signature(); - jwt::algorithm::rs256 alg(TestPublicKeyContent); - std::error_code ec; - alg.verify(data, signature, ec); - if (ec) { - throw std::runtime_error(ec.message()); - } + auto t = source->GetToken(); + UNIT_ASSERT_VALUES_EQUAL(t.TokenType, "urn:ietf:params:oauth:token-type:jwt"); + check.Check(t.Token); } Y_UNIT_TEST(BadParams) { @@ -60,13 +48,13 @@ Y_UNIT_TEST_SUITE(JwtTokenSourceTest) { UNIT_ASSERT_EXCEPTION_CONTAINS(CreateJwtTokenSource( TJwtTokenSourceParams() .KeyId("test_key_id") - .SigningAlgorithm("", TestPrivateKeyContent) + .SigningAlgorithm("", TestRSAPrivateKeyContent) .TokenTtl(TDuration::Zero()) ), std::invalid_argument, "token TTL must be positive"); UNIT_ASSERT_EXCEPTION_CONTAINS(CreateJwtTokenSource( TJwtTokenSourceParams() - .SigningAlgorithm("", TestPrivateKeyContent) + .SigningAlgorithm("", TestRSAPrivateKeyContent) .AppendAudience("aud") .AppendAudience("aud2") .AppendAudience("")