From ae9c87eff3239abad916f1de688483e950b5b091 Mon Sep 17 00:00:00 2001 From: Maxim Kolganov Date: Mon, 18 Jul 2022 11:31:19 +0300 Subject: [PATCH 1/3] Support IAM tokens as authorisation method --- Makefile | 22 ++++++++++---------- README.md | 10 ++++++++-- tests/test_service_account_auth.py | 7 ++++++- yandexcloud/_auth_fabric.py | 32 +++++++++++++++++++++++------- yandexcloud/_channels.py | 4 +++- 5 files changed, 53 insertions(+), 22 deletions(-) diff --git a/Makefile b/Makefile index ac234762..4700f505 100644 --- a/Makefile +++ b/Makefile @@ -4,29 +4,29 @@ REPO_ROOT:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) deps: ## install deps (library & development) - python -m pip install --upgrade pip - python -m pip install -r requirements-dev.txt + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements-dev.txt deps-genproto: ## install deps (library & development) - python -m pip install --upgrade pip - python -m pip install -r requirements-genproto.txt + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements-genproto.txt tox: ## run ALL checks for ALL available python versions - python -m tox + python3 -m tox tox-current: ## run ALL checks ONLY for current python version - python -m tox -e `python -c 'import platform; print("py" + "".join(platform.python_version_tuple()[:2]))'` + python3 -m tox -e `python3 -c 'import platform; print("py" + "".join(platform.python_version_tuple()[:2]))'` test: ## run tests ONLY for current python version - python -m pytest + python3 -m pytest lint: ## run linters, formatters for current python versions - python -m flake8 yandexcloud - python -m pylint yandexcloud + python3 -m flake8 yandexcloud + python3 -m pylint yandexcloud format: - python -m isort yandexcloud setup.py changelog.py - python -m black yandexcloud setup.py changelog.py + python3 -m isort yandexcloud setup.py changelog.py + python3 -m black yandexcloud setup.py changelog.py test-all-versions: ## run test for multiple python versions using docker # python 3.10 not provided in image so we skip it diff --git a/README.md b/README.md index 5e324d92..97288777 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Installation: ## Getting started There are several options for authorization your requests - OAuth Token, -Metadata Service (if you're executing code inside VMs or Functions -running in Yandex.Cloud) and Service Account Keys +Metadata Service (if you're executing your code inside VMs or Cloud Functions +running in Yandex.Cloud), Service Account Keys, and externally created IAM tokens. ### OAuth Token @@ -52,6 +52,12 @@ sa_key = { sdk = yandexcloud.SDK(service_account_key=sa_key) ``` +### IAM tokens + +```python +sdk = yandexcloud.SDK(iam_token="t1.9eu...") +``` + Check `examples` directory for more examples. diff --git a/tests/test_service_account_auth.py b/tests/test_service_account_auth.py index 2e145104..3ab4599d 100644 --- a/tests/test_service_account_auth.py +++ b/tests/test_service_account_auth.py @@ -9,7 +9,7 @@ def test_both_params_error(token, service_account_key): with pytest.raises(RuntimeError) as e: get_auth_token_requester(token=token, service_account_key=service_account_key).get_token_request() - assert str(e.value) == "Conflicting API credentials properties 'token' and 'service-account-key' are set." + assert str(e.value) == "Conflicting API credentials properties are set: ['token', 'service_account_key']." def test_invalid_service_account_type(): @@ -57,3 +57,8 @@ def test_service_account_key(service_account_key): assert parsed["iss"] == service_account_key["service_account_id"] assert parsed["aud"] == "https://iam.api.cloud.yandex.net/iam/v1/tokens" assert now - 60 <= int(parsed["iat"]) <= now + +def test_iam_token(iam_token): + token_func = get_auth_token_requester(iam_token=iam_token).get_token + token = token_func() + assert token == iam_token \ No newline at end of file diff --git a/yandexcloud/_auth_fabric.py b/yandexcloud/_auth_fabric.py index 6b1a7a7b..2767f03e 100644 --- a/yandexcloud/_auth_fabric.py +++ b/yandexcloud/_auth_fabric.py @@ -43,18 +43,28 @@ def __validate_service_account_key(sa_key): raise RuntimeError(error_message) -def get_auth_token_requester(token=None, service_account_key=None, metadata_addr=_MDS_ADDR): - if token is not None and service_account_key is not None: - raise RuntimeError("Conflicting API credentials properties 'token' and 'service-account-key' are set.") +def get_auth_token_requester(token=None, service_account_key=None, iam_token=None, metadata_addr=_MDS_ADDR): + auth_methods = [("token", token), ("service_account_key", service_account_key), ("iam_token", iam_token)] + auth_methods = [auth for auth in auth_methods if auth[1] is not None] - if token is not None: - return TokenAuth(token=token) + if len(auth_methods) == 0: + return MetadataAuth(metadata_addr=metadata_addr) + + if len(auth_methods) > 1: + raise RuntimeError( + "Conflicting API credentials properties are set: {}.".format([auth[0] for auth in auth_methods]) + ) - if service_account_key is not None: + (auth_name, _) = auth_methods[0] + if auth_name == "token": + return TokenAuth(token=token) + elif auth_name == "service_account_key": __validate_service_account_key(service_account_key) return ServiceAccountAuth(service_account_key) + elif auth_name == "iam_token": + return IamTokenAuth(iam_token) - return MetadataAuth(metadata_addr=metadata_addr) + raise RuntimeError("Unknown auth method: {}".format(auth_name)) class MetadataAuth: @@ -106,3 +116,11 @@ def __prepare_request(self): } return jwt.encode(payload, self.__sa_key["private_key"], algorithm="PS256", headers=headers) + + +class IamTokenAuth: + def __init__(self, iam_token): + self.__iam_token = iam_token + + def get_token(self): + return self.__iam_token diff --git a/yandexcloud/_channels.py b/yandexcloud/_channels.py index 4ad3cba7..a539f581 100644 --- a/yandexcloud/_channels.py +++ b/yandexcloud/_channels.py @@ -23,7 +23,9 @@ def __init__(self, client_user_agent=None, **kwargs): ) self._endpoint = kwargs.get("endpoint", "api.cloud.yandex.net") self._token_requester = get_auth_token_requester( - token=kwargs.get("token"), service_account_key=kwargs.get("service_account_key") + token=kwargs.get("token"), + service_account_key=kwargs.get("service_account_key"), + iam_token=kwargs.get("iam_token"), ) self._unauthenticated_channel = None From 5ec6fe9beb8fe6b4854e3f84ff2fc75d451d4e07 Mon Sep 17 00:00:00 2001 From: Maxim Kolganov Date: Mon, 18 Jul 2022 11:33:11 +0300 Subject: [PATCH 2/3] remove elif after return --- yandexcloud/_auth_fabric.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yandexcloud/_auth_fabric.py b/yandexcloud/_auth_fabric.py index 2767f03e..fbb33422 100644 --- a/yandexcloud/_auth_fabric.py +++ b/yandexcloud/_auth_fabric.py @@ -58,10 +58,10 @@ def get_auth_token_requester(token=None, service_account_key=None, iam_token=Non (auth_name, _) = auth_methods[0] if auth_name == "token": return TokenAuth(token=token) - elif auth_name == "service_account_key": + if auth_name == "service_account_key": __validate_service_account_key(service_account_key) return ServiceAccountAuth(service_account_key) - elif auth_name == "iam_token": + if auth_name == "iam_token": return IamTokenAuth(iam_token) raise RuntimeError("Unknown auth method: {}".format(auth_name)) From 76ebf92004bd15534edb682d34067522d5c01adb Mon Sep 17 00:00:00 2001 From: Maxim Kolganov Date: Mon, 18 Jul 2022 16:06:01 +0300 Subject: [PATCH 3/3] address review comments --- yandexcloud/_auth_fabric.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yandexcloud/_auth_fabric.py b/yandexcloud/_auth_fabric.py index fbb33422..ea99d28b 100644 --- a/yandexcloud/_auth_fabric.py +++ b/yandexcloud/_auth_fabric.py @@ -45,7 +45,7 @@ def __validate_service_account_key(sa_key): def get_auth_token_requester(token=None, service_account_key=None, iam_token=None, metadata_addr=_MDS_ADDR): auth_methods = [("token", token), ("service_account_key", service_account_key), ("iam_token", iam_token)] - auth_methods = [auth for auth in auth_methods if auth[1] is not None] + auth_methods = [(auth_type, value) for auth_type, value in auth_methods if value is not None] if len(auth_methods) == 0: return MetadataAuth(metadata_addr=metadata_addr) @@ -55,7 +55,7 @@ def get_auth_token_requester(token=None, service_account_key=None, iam_token=Non "Conflicting API credentials properties are set: {}.".format([auth[0] for auth in auth_methods]) ) - (auth_name, _) = auth_methods[0] + auth_name, _ = auth_methods[0] if auth_name == "token": return TokenAuth(token=token) if auth_name == "service_account_key":