diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae7391e..70bb5bc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +* OAuth 2.0 token exchange. Allow multiple resource parameters in according to https://www.rfc-editor.org/rfc/rfc8693 + ## 3.14.0 ## * Added load OAuth 2.0 token exchange credentials provider from config file @@ -83,7 +85,7 @@ yanked bad api release ## 3.3.5 ## * Fixed use positional argument instead of named in WriterAsyncIO.__del__ -* Fixed release buffer while read topic by one messages +* Fixed release buffer while read topic by one messages * Fixed race condition between commit_with_ack and reconnect in topic writer ## 3.3.4 ## diff --git a/tests/oauth2_token_exchange/test_token_exchange.py b/tests/oauth2_token_exchange/test_token_exchange.py index 010a5d42..51270ba1 100644 --- a/tests/oauth2_token_exchange/test_token_exchange.py +++ b/tests/oauth2_token_exchange/test_token_exchange.py @@ -181,11 +181,12 @@ def check(self, handler, parsed_request) -> None: assert len(parsed_request.get("scope", [])) == 1 assert parsed_request["scope"][0] == " ".join(self.scope) - if self.resource is None or self.resource == "": + if self.resource is None or len(self.resource) == 0: assert len(parsed_request.get("resource", [])) == 0 else: - assert len(parsed_request.get("resource", [])) == 1 - assert parsed_request["resource"][0] == self.resource + assert len(parsed_request["resource"]) == len(self.resource) + for i in range(len(self.resource)): + assert parsed_request["resource"][i] == self.resource[i] if self.subject_token_source is None: assert len(parsed_request.get("subject_token", [])) == 0 @@ -478,7 +479,7 @@ def test_oauth2_token_exchange_credentials_file(): ), grant_type="grant", requested_token_type="access_token", - resource="tEst", + resource=["tEst"], ), response=Oauth2TokenExchangeResponse( 200, @@ -498,6 +499,7 @@ def test_oauth2_token_exchange_credentials_file(): "s1", "s2", ], + "res": ["r1", "r2"], "unknown-field": [123], "actor-credentials": { "type": "fixed", @@ -512,6 +514,7 @@ def test_oauth2_token_exchange_credentials_file(): ), audience=["test-aud"], scope=["s1", "s2"], + resource=["r1", "r2"], ), response=Oauth2TokenExchangeResponse( 200, diff --git a/ydb/oauth2_token_exchange/token_exchange.py b/ydb/oauth2_token_exchange/token_exchange.py index 8f16619d..819f719b 100644 --- a/ydb/oauth2_token_exchange/token_exchange.py +++ b/ydb/oauth2_token_exchange/token_exchange.py @@ -39,7 +39,7 @@ def __init__( actor_token_source: typing.Optional[TokenSource] = None, audience: typing.Union[typing.List[str], str, None] = None, scope: typing.Union[typing.List[str], str, None] = None, - resource: typing.Optional[str] = None, + resource: typing.Union[typing.List[str], str, None] = None, grant_type: str = "urn:ietf:params:oauth:grant-type:token-exchange", requested_token_type: str = "urn:ietf:params:oauth:token-type:access_token", ): @@ -224,6 +224,42 @@ def _duration_seconds_from_config(cls, cfg_json, key_name, default_value): @classmethod def from_file(cls, cfg_file, iam_endpoint=None): + """ + Create OAuth 2.0 token exchange protocol credentials from config file. + + 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: "urn:ietf:params:oauth:grant-type:token-exchange") + 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: "urn:ietf:params:oauth:token-type:access_token") + subject-credentials: [creds_json] Subject credentials options (optional) + actor-credentials: [creds_json] Actor credentials options (optional) + token-endpoint: [string] Token endpoint + + 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 (RSA, EC) or Base64 format (HMAC) 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: 1h) + + 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) + """ with open(os.path.expanduser(cfg_file), "r") as r: cfg = r.read() @@ -245,7 +281,7 @@ def from_content(cls, cfg, iam_endpoint=None): actor_token_source = cls._token_source_from_config(cfg_json, "actor-credentials") audience = cls._list_of_strings_or_single_from_config(cfg_json, "aud") scope = cls._list_of_strings_or_single_from_config(cfg_json, "scope") - resource = cls._string_with_default_from_config(cfg_json, "res", None) + resource = cls._list_of_strings_or_single_from_config(cfg_json, "res") grant_type = cls._string_with_default_from_config( cfg_json, "grant-type", "urn:ietf:params:oauth:grant-type:token-exchange" ) @@ -273,7 +309,7 @@ def __init__( actor_token_source: typing.Optional[TokenSource] = None, audience: typing.Union[typing.List[str], str, None] = None, scope: typing.Union[typing.List[str], str, None] = None, - resource: typing.Optional[str] = None, + resource: typing.Union[typing.List[str], str, None] = None, grant_type: str = "urn:ietf:params:oauth:grant-type:token-exchange", requested_token_type: str = "urn:ietf:params:oauth:token-type:access_token", tracer=None,