diff --git a/sdk/keyvault/azure-keyvault-administration/CHANGELOG.md b/sdk/keyvault/azure-keyvault-administration/CHANGELOG.md index 5bbecd93b402..2b87d2e07414 100644 --- a/sdk/keyvault/azure-keyvault-administration/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-administration/CHANGELOG.md @@ -3,12 +3,15 @@ ## 4.1.0b2 (Unreleased) ### Features Added +- Added support for multi-tenant authentication when using `azure-identity` 1.7.1 or newer + ([#20698](https://github.com/Azure/azure-sdk-for-python/issues/20698)) ### Breaking Changes ### Bugs Fixed ### Other Changes +- Updated minimum `azure-core` version to 1.15.0 ## 4.1.0b1 (2021-09-09) diff --git a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/__init__.py b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/__init__.py index f73bf01e0e89..905e81953480 100644 --- a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/__init__.py +++ b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/__init__.py @@ -6,7 +6,7 @@ from six.moves.urllib_parse import urlparse -from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase +from .challenge_auth_policy import ChallengeAuthPolicy from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache @@ -14,7 +14,6 @@ __all__ = [ "ChallengeAuthPolicy", - "ChallengeAuthPolicyBase", "HttpChallenge", "HttpChallengeCache", "KeyVaultClientBase", diff --git a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py index 97f1d093e20f..bfe46689f5ff 100644 --- a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py @@ -13,67 +13,69 @@ requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the protocol again. """ + +import time from typing import TYPE_CHECKING -from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from . import HttpChallengeCache -from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _update_challenge if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional + from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential - from azure.core.pipeline import PipelineRequest - from azure.core.pipeline.transport import AsyncHttpResponse - from . import HttpChallenge + from azure.core.pipeline import PipelineRequest, PipelineResponse -class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: + def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: + super().__init__(credential, *scopes, **kwargs) self._credential = credential - super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": + async def on_request(self, request: "PipelineRequest") -> None: _enforce_tls(request) - - challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = await self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - return response - - async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + + async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = await self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) + + return True - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + @property + def _need_new_token(self) -> bool: + # pylint:disable=invalid-overridden-method + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py index 3239032e9162..7f6f2b93f0e4 100644 --- a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py @@ -14,13 +14,11 @@ protocol again. """ -import copy import time from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineContext, PipelineRequest -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpRequest +from azure.core.pipeline import PipelineRequest +from azure.core.pipeline.policies import BearerTokenCredentialPolicy from .http_challenge import HttpChallenge from . import http_challenge_cache as ChallengeCache @@ -44,22 +42,6 @@ def _enforce_tls(request): ) -def _get_challenge_request(request): - # type: (PipelineRequest) -> PipelineRequest - - # The challenge request is intended to provoke an authentication challenge from Key Vault, to learn how the - # service request should be authenticated. It should be identical to the service request but with no body. - challenge_request = HttpRequest( - request.http_request.method, request.http_request.url, headers=request.http_request.headers - ) - challenge_request.headers["Content-Length"] = "0" - - options = copy.deepcopy(request.context.options) - context = PipelineContext(request.context.transport, **options) - - return PipelineRequest(http_request=challenge_request, context=context) - - def _update_challenge(request, challenger): # type: (PipelineRequest, PipelineResponse) -> HttpChallenge """parse challenge from challenger, cache it, return it""" @@ -73,68 +55,55 @@ def _update_challenge(request, challenger): return challenge -class ChallengeAuthPolicyBase(object): - """Sans I/O base for challenge authentication policies""" - - def __init__(self, **kwargs): - self._token = None # type: Optional[AccessToken] - super(ChallengeAuthPolicyBase, self).__init__(**kwargs) - - @property - def _need_new_token(self): - # type: () -> bool - return not self._token or self._token.expires_on - time.time() < 300 - - -class ChallengeAuthPolicy(ChallengeAuthPolicyBase, HTTPPolicy): +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential, **kwargs): - # type: (TokenCredential, **Any) -> None + def __init__(self, credential, *scopes, **kwargs): + # type: (TokenCredential, *str, **Any) -> None + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, **kwargs) self._credential = credential - super(ChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - def send(self, request): - # type: (PipelineRequest) -> PipelineResponse + def on_request(self, request): + # type: (PipelineRequest) -> None _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - return response - - def _handle_challenge(self, request, challenge): - # type: (PipelineRequest, HttpChallenge) -> None - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + def on_challenge(self, request, response): + # type: (PipelineRequest, PipelineResponse) -> bool + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + self.authorize_request(request, scope, tenant_id=challenge.tenant_id) - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return True + + @property + def _need_new_token(self): + # type: () -> bool + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge.py b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge.py index c762e1ae50ef..c52c90929ad9 100644 --- a/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge.py +++ b/sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge.py @@ -40,6 +40,11 @@ def __init__(self, request_uri, challenge, response_headers=None): if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: raise ValueError("Invalid challenge parameters") + authorization_uri = self.get_authorization_server() + # the authoritzation server URI should look something like https://login.windows.net/tenant-id + uri_path = parse.urlparse(authorization_uri).path.lstrip("/") + self.tenant_id = uri_path.split("/")[0] or None + # if the response headers were supplied if response_headers: # get the message signing key and message key encryption key from the headers diff --git a/sdk/keyvault/azure-keyvault-administration/setup.py b/sdk/keyvault/azure-keyvault-administration/setup.py index 0231b074702c..9016754edd4b 100644 --- a/sdk/keyvault/azure-keyvault-administration/setup.py +++ b/sdk/keyvault/azure-keyvault-administration/setup.py @@ -81,7 +81,7 @@ "azure.keyvault", ] ), - install_requires=["azure-common~=1.1", "azure-core<2.0.0,>=1.11.0", "msrest>=0.6.21", "six>=1.11.0"], + install_requires=["azure-common~=1.1", "azure-core<2.0.0,>=1.15.0", "msrest>=0.6.21", "six>=1.11.0"], extras_require={ ":python_version<'3.0'": ["azure-keyvault-nspkg"], ":python_version<'3.4'": ["enum34>=1.0.4"], diff --git a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md index f399e57e03c3..e187f020b952 100644 --- a/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md @@ -3,12 +3,15 @@ ## 4.4.0b2 (Unreleased) ### Features Added +- Added support for multi-tenant authentication when using `azure-identity` 1.7.1 or newer + ([#20698](https://github.com/Azure/azure-sdk-for-python/issues/20698)) ### Breaking Changes ### Bugs Fixed ### Other Changes +- Updated minimum `azure-core` version to 1.15.0 ## 4.4.0b1 (2021-09-09) diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py index 3329ce2df068..d8303b4b081d 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/__init__.py @@ -9,7 +9,7 @@ import urlparse as parse # type: ignore from typing import TYPE_CHECKING -from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase +from .challenge_auth_policy import ChallengeAuthPolicy from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache @@ -21,7 +21,6 @@ __all__ = [ "ChallengeAuthPolicy", - "ChallengeAuthPolicyBase", "HttpChallenge", "HttpChallengeCache", "KeyVaultClientBase", diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py index 97f1d093e20f..bfe46689f5ff 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py @@ -13,67 +13,69 @@ requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the protocol again. """ + +import time from typing import TYPE_CHECKING -from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from . import HttpChallengeCache -from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _update_challenge if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional + from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential - from azure.core.pipeline import PipelineRequest - from azure.core.pipeline.transport import AsyncHttpResponse - from . import HttpChallenge + from azure.core.pipeline import PipelineRequest, PipelineResponse -class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: + def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: + super().__init__(credential, *scopes, **kwargs) self._credential = credential - super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": + async def on_request(self, request: "PipelineRequest") -> None: _enforce_tls(request) - - challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = await self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - return response - - async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + + async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = await self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) + + return True - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + @property + def _need_new_token(self) -> bool: + # pylint:disable=invalid-overridden-method + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py index 3239032e9162..7f6f2b93f0e4 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py @@ -14,13 +14,11 @@ protocol again. """ -import copy import time from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineContext, PipelineRequest -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpRequest +from azure.core.pipeline import PipelineRequest +from azure.core.pipeline.policies import BearerTokenCredentialPolicy from .http_challenge import HttpChallenge from . import http_challenge_cache as ChallengeCache @@ -44,22 +42,6 @@ def _enforce_tls(request): ) -def _get_challenge_request(request): - # type: (PipelineRequest) -> PipelineRequest - - # The challenge request is intended to provoke an authentication challenge from Key Vault, to learn how the - # service request should be authenticated. It should be identical to the service request but with no body. - challenge_request = HttpRequest( - request.http_request.method, request.http_request.url, headers=request.http_request.headers - ) - challenge_request.headers["Content-Length"] = "0" - - options = copy.deepcopy(request.context.options) - context = PipelineContext(request.context.transport, **options) - - return PipelineRequest(http_request=challenge_request, context=context) - - def _update_challenge(request, challenger): # type: (PipelineRequest, PipelineResponse) -> HttpChallenge """parse challenge from challenger, cache it, return it""" @@ -73,68 +55,55 @@ def _update_challenge(request, challenger): return challenge -class ChallengeAuthPolicyBase(object): - """Sans I/O base for challenge authentication policies""" - - def __init__(self, **kwargs): - self._token = None # type: Optional[AccessToken] - super(ChallengeAuthPolicyBase, self).__init__(**kwargs) - - @property - def _need_new_token(self): - # type: () -> bool - return not self._token or self._token.expires_on - time.time() < 300 - - -class ChallengeAuthPolicy(ChallengeAuthPolicyBase, HTTPPolicy): +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential, **kwargs): - # type: (TokenCredential, **Any) -> None + def __init__(self, credential, *scopes, **kwargs): + # type: (TokenCredential, *str, **Any) -> None + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, **kwargs) self._credential = credential - super(ChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - def send(self, request): - # type: (PipelineRequest) -> PipelineResponse + def on_request(self, request): + # type: (PipelineRequest) -> None _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - return response - - def _handle_challenge(self, request, challenge): - # type: (PipelineRequest, HttpChallenge) -> None - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + def on_challenge(self, request, response): + # type: (PipelineRequest, PipelineResponse) -> bool + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + self.authorize_request(request, scope, tenant_id=challenge.tenant_id) - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return True + + @property + def _need_new_token(self): + # type: () -> bool + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py index c762e1ae50ef..c52c90929ad9 100644 --- a/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py +++ b/sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge.py @@ -40,6 +40,11 @@ def __init__(self, request_uri, challenge, response_headers=None): if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: raise ValueError("Invalid challenge parameters") + authorization_uri = self.get_authorization_server() + # the authoritzation server URI should look something like https://login.windows.net/tenant-id + uri_path = parse.urlparse(authorization_uri).path.lstrip("/") + self.tenant_id = uri_path.split("/")[0] or None + # if the response headers were supplied if response_headers: # get the message signing key and message key encryption key from the headers diff --git a/sdk/keyvault/azure-keyvault-certificates/setup.py b/sdk/keyvault/azure-keyvault-certificates/setup.py index e9d54c896c34..5eeceb33b647 100644 --- a/sdk/keyvault/azure-keyvault-certificates/setup.py +++ b/sdk/keyvault/azure-keyvault-certificates/setup.py @@ -83,7 +83,7 @@ ] ), install_requires=[ - "azure-core<2.0.0,>=1.7.0", + "azure-core<2.0.0,>=1.15.0", "msrest>=0.6.21", "azure-common~=1.1", ], diff --git a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md index 8e3c353635c3..2320c33e2275 100644 --- a/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-keys/CHANGELOG.md @@ -3,12 +3,15 @@ ## 4.5.0b5 (Unreleased) ### Features Added +- Added support for multi-tenant authentication when using `azure-identity` 1.7.1 or newer + ([#20698](https://github.com/Azure/azure-sdk-for-python/issues/20698)) ### Breaking Changes ### Bugs Fixed ### Other Changes +- Updated minimum `azure-core` version to 1.15.0 ## 4.5.0b4 (2021-10-07) diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py index 3329ce2df068..d8303b4b081d 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/__init__.py @@ -9,7 +9,7 @@ import urlparse as parse # type: ignore from typing import TYPE_CHECKING -from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase +from .challenge_auth_policy import ChallengeAuthPolicy from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache @@ -21,7 +21,6 @@ __all__ = [ "ChallengeAuthPolicy", - "ChallengeAuthPolicyBase", "HttpChallenge", "HttpChallengeCache", "KeyVaultClientBase", diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py index 97f1d093e20f..bfe46689f5ff 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/async_challenge_auth_policy.py @@ -13,67 +13,69 @@ requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the protocol again. """ + +import time from typing import TYPE_CHECKING -from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from . import HttpChallengeCache -from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _update_challenge if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional + from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential - from azure.core.pipeline import PipelineRequest - from azure.core.pipeline.transport import AsyncHttpResponse - from . import HttpChallenge + from azure.core.pipeline import PipelineRequest, PipelineResponse -class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: + def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: + super().__init__(credential, *scopes, **kwargs) self._credential = credential - super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": + async def on_request(self, request: "PipelineRequest") -> None: _enforce_tls(request) - - challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = await self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - return response - - async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + + async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = await self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) + + return True - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + @property + def _need_new_token(self) -> bool: + # pylint:disable=invalid-overridden-method + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py index 3239032e9162..7f6f2b93f0e4 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py @@ -14,13 +14,11 @@ protocol again. """ -import copy import time from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineContext, PipelineRequest -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpRequest +from azure.core.pipeline import PipelineRequest +from azure.core.pipeline.policies import BearerTokenCredentialPolicy from .http_challenge import HttpChallenge from . import http_challenge_cache as ChallengeCache @@ -44,22 +42,6 @@ def _enforce_tls(request): ) -def _get_challenge_request(request): - # type: (PipelineRequest) -> PipelineRequest - - # The challenge request is intended to provoke an authentication challenge from Key Vault, to learn how the - # service request should be authenticated. It should be identical to the service request but with no body. - challenge_request = HttpRequest( - request.http_request.method, request.http_request.url, headers=request.http_request.headers - ) - challenge_request.headers["Content-Length"] = "0" - - options = copy.deepcopy(request.context.options) - context = PipelineContext(request.context.transport, **options) - - return PipelineRequest(http_request=challenge_request, context=context) - - def _update_challenge(request, challenger): # type: (PipelineRequest, PipelineResponse) -> HttpChallenge """parse challenge from challenger, cache it, return it""" @@ -73,68 +55,55 @@ def _update_challenge(request, challenger): return challenge -class ChallengeAuthPolicyBase(object): - """Sans I/O base for challenge authentication policies""" - - def __init__(self, **kwargs): - self._token = None # type: Optional[AccessToken] - super(ChallengeAuthPolicyBase, self).__init__(**kwargs) - - @property - def _need_new_token(self): - # type: () -> bool - return not self._token or self._token.expires_on - time.time() < 300 - - -class ChallengeAuthPolicy(ChallengeAuthPolicyBase, HTTPPolicy): +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential, **kwargs): - # type: (TokenCredential, **Any) -> None + def __init__(self, credential, *scopes, **kwargs): + # type: (TokenCredential, *str, **Any) -> None + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, **kwargs) self._credential = credential - super(ChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - def send(self, request): - # type: (PipelineRequest) -> PipelineResponse + def on_request(self, request): + # type: (PipelineRequest) -> None _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - return response - - def _handle_challenge(self, request, challenge): - # type: (PipelineRequest, HttpChallenge) -> None - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + def on_challenge(self, request, response): + # type: (PipelineRequest, PipelineResponse) -> bool + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + self.authorize_request(request, scope, tenant_id=challenge.tenant_id) - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return True + + @property + def _need_new_token(self): + # type: () -> bool + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py index c762e1ae50ef..c52c90929ad9 100644 --- a/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py +++ b/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge.py @@ -40,6 +40,11 @@ def __init__(self, request_uri, challenge, response_headers=None): if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: raise ValueError("Invalid challenge parameters") + authorization_uri = self.get_authorization_server() + # the authoritzation server URI should look something like https://login.windows.net/tenant-id + uri_path = parse.urlparse(authorization_uri).path.lstrip("/") + self.tenant_id = uri_path.split("/")[0] or None + # if the response headers were supplied if response_headers: # get the message signing key and message key encryption key from the headers diff --git a/sdk/keyvault/azure-keyvault-keys/setup.py b/sdk/keyvault/azure-keyvault-keys/setup.py index 3a12c3d53b5d..400544558a44 100644 --- a/sdk/keyvault/azure-keyvault-keys/setup.py +++ b/sdk/keyvault/azure-keyvault-keys/setup.py @@ -82,7 +82,7 @@ ] ), install_requires=[ - "azure-core<2.0.0,>=1.7.0", + "azure-core<2.0.0,>=1.15.0", "cryptography>=2.1.4", "msrest>=0.6.21", "azure-common~=1.1", diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_mhsm.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_mhsm.yaml new file mode 100644 index 000000000000..01d1a918d4e9 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_mhsm.yaml @@ -0,0 +1,173 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/create?api-version=7.3-preview + response: + body: + string: '' + headers: + cache-control: + - no-cache + content-length: + - '0' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + www-authenticate: + - Bearer authorization="https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://managedhsm.azure.net" + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-server-latency: + - '1' + status: + code: 401 + message: Unauthorized +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/create?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636415841,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636415841},"key":{"e":"AQAB","key_ops":["wrapKey","sign","verify","encrypt","decrypt","unwrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/818c3c2903cc06289eaacf1a2070d5bd","kty":"RSA-HSM","n":"ncexSVGCn8VKmtNlZJAtTYK80rexx2xO_v_WOFuqCz3VYxqy7bzU2dsL3PWp66SqdU-OEXJ3jfuvyk4JCO2uYf8QF-7kVKCeB0N93pqh-2hMi_mDVaT0iMpB-USuJA34K0tDsouDa1B86WlXDeh1OBMFM4H_tVgDwgwS-zHXfM0L2UmTjekXSj5XRG2vaUztlIxfYKRrw4lSi5sRLv7VXiS-HWZG4x1U9miyCKtPdqAWqkqZ9O5yOQZ-B0Hk5yeO8TWbfMgyUc61H8ga3p0KExiLJatSCNPaxeOzlf-Eqb2g5pEFR74Nk9d_HkvW0bWs1elcMxZPehG_aF76U3GYwQ"}}' + headers: + cache-control: + - no-cache + content-length: + - '741' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: + - westus + x-ms-server-latency: + - '393' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636415841,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636415841},"key":{"e":"AQAB","key_ops":["unwrapKey","decrypt","encrypt","verify","sign","wrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/818c3c2903cc06289eaacf1a2070d5bd","kty":"RSA-HSM","n":"ncexSVGCn8VKmtNlZJAtTYK80rexx2xO_v_WOFuqCz3VYxqy7bzU2dsL3PWp66SqdU-OEXJ3jfuvyk4JCO2uYf8QF-7kVKCeB0N93pqh-2hMi_mDVaT0iMpB-USuJA34K0tDsouDa1B86WlXDeh1OBMFM4H_tVgDwgwS-zHXfM0L2UmTjekXSj5XRG2vaUztlIxfYKRrw4lSi5sRLv7VXiS-HWZG4x1U9miyCKtPdqAWqkqZ9O5yOQZ-B0Hk5yeO8TWbfMgyUc61H8ga3p0KExiLJatSCNPaxeOzlf-Eqb2g5pEFR74Nk9d_HkvW0bWs1elcMxZPehG_aF76U3GYwQ"}}' + headers: + cache-control: + - no-cache + content-length: + - '741' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-build-version: + - 1.0.20210929-1-5b78c022-develop + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: + - westus + x-ms-server-latency: + - '90' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636415841,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636415841},"key":{"e":"AQAB","key_ops":["unwrapKey","decrypt","encrypt","verify","sign","wrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-keybfb31bb2/818c3c2903cc06289eaacf1a2070d5bd","kty":"RSA-HSM","n":"ncexSVGCn8VKmtNlZJAtTYK80rexx2xO_v_WOFuqCz3VYxqy7bzU2dsL3PWp66SqdU-OEXJ3jfuvyk4JCO2uYf8QF-7kVKCeB0N93pqh-2hMi_mDVaT0iMpB-USuJA34K0tDsouDa1B86WlXDeh1OBMFM4H_tVgDwgwS-zHXfM0L2UmTjekXSj5XRG2vaUztlIxfYKRrw4lSi5sRLv7VXiS-HWZG4x1U9miyCKtPdqAWqkqZ9O5yOQZ-B0Hk5yeO8TWbfMgyUc61H8ga3p0KExiLJatSCNPaxeOzlf-Eqb2g5pEFR74Nk9d_HkvW0bWs1elcMxZPehG_aF76U3GYwQ"}}' + headers: + cache-control: + - no-cache + content-length: + - '741' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-build-version: + - 1.0.20210929-1-5b78c022-develop + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: + - westus + x-ms-server-latency: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_vault.yaml new file mode 100644 index 000000000000..c251debdac2a --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth.test_multitenant_authentication_7_3_preview_vault.yaml @@ -0,0 +1,190 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/create?api-version=7.3-preview + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: + - no-cache + content-length: + - '97' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Nov 2021 23:57:24 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + www-authenticate: + - Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.9.150.1 + x-powered-by: + - ASP.NET + status: + code: 401 + message: Unauthorized +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/create?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/fae15351fde54b7384e4145ab181b03f","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"yP2jt3YHJ3Kc46lv-pZtzI0iZZkbsXVwmO-GAe3kBlPeyzbFI6oT7KeUuwtVW0410mOdOUGCGWVk_BoXW09s49ScUPz0JPA8Hyc64y3MgW2u8frHWLq6EJsB7c4Sjz0oK2HwdNqLgRsmQ4DdzoXp5os2NQ0qt_hB39VNR1RL9J_25xevw7VUGiQfOcEYwTKYYnhLoxK_j74oRbd42-Ecck8riN76kL--Sp-3bqNX2fx3HzQVr8Vo4_wL84Stj64gAAJZ3q4J53qNeUfx7e99qJvBFBICTWiQq5b3y0wkcAxk6Bhq_2MB_WnZReaZ3pCm-XhJJU9MqwQ1Phkgb8bv8Q","e":"AQAB"},"attributes":{"enabled":true,"created":1636415845,"updated":1636415845,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: + - no-cache + content-length: + - '701' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Nov 2021 23:57:25 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.9.150.1 + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/fae15351fde54b7384e4145ab181b03f","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"yP2jt3YHJ3Kc46lv-pZtzI0iZZkbsXVwmO-GAe3kBlPeyzbFI6oT7KeUuwtVW0410mOdOUGCGWVk_BoXW09s49ScUPz0JPA8Hyc64y3MgW2u8frHWLq6EJsB7c4Sjz0oK2HwdNqLgRsmQ4DdzoXp5os2NQ0qt_hB39VNR1RL9J_25xevw7VUGiQfOcEYwTKYYnhLoxK_j74oRbd42-Ecck8riN76kL--Sp-3bqNX2fx3HzQVr8Vo4_wL84Stj64gAAJZ3q4J53qNeUfx7e99qJvBFBICTWiQq5b3y0wkcAxk6Bhq_2MB_WnZReaZ3pCm-XhJJU9MqwQ1Phkgb8bv8Q","e":"AQAB"},"attributes":{"enabled":true,"created":1636415845,"updated":1636415845,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: + - no-cache + content-length: + - '701' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Nov 2021 23:57:25 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.9.150.1 + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keydbee1c29/fae15351fde54b7384e4145ab181b03f","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"yP2jt3YHJ3Kc46lv-pZtzI0iZZkbsXVwmO-GAe3kBlPeyzbFI6oT7KeUuwtVW0410mOdOUGCGWVk_BoXW09s49ScUPz0JPA8Hyc64y3MgW2u8frHWLq6EJsB7c4Sjz0oK2HwdNqLgRsmQ4DdzoXp5os2NQ0qt_hB39VNR1RL9J_25xevw7VUGiQfOcEYwTKYYnhLoxK_j74oRbd42-Ecck8riN76kL--Sp-3bqNX2fx3HzQVr8Vo4_wL84Stj64gAAJZ3q4J53qNeUfx7e99qJvBFBICTWiQq5b3y0wkcAxk6Bhq_2MB_WnZReaZ3pCm-XhJJU9MqwQ1Phkgb8bv8Q","e":"AQAB"},"attributes":{"enabled":true,"created":1636415845,"updated":1636415845,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: + - no-cache + content-length: + - '701' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Nov 2021 23:57:25 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000;includeSubDomains + x-content-type-options: + - nosniff + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: + - westus + x-ms-keyvault-service-version: + - 1.9.150.1 + x-powered-by: + - ASP.NET + status: + code: 200 + message: OK +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_2016_10_01_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_2016_10_01_vault.yaml new file mode 100644 index 000000000000..eb3682928b17 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_2016_10_01_vault.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/create?api-version=2016-10-01 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:28 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/create?api-version=2016-10-01 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/create?api-version=2016-10-01 + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/2fe8efc82a53428b9d5bbc5ea90dc276","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"6qDo5N0hCCRwO1IVj0XFfjFsJjno0YClIjURSlKFUOX7YIluWkw2o6RUKGyDQujcYpVj9Q8Y6NfeZn3quJUBIRHa02ir0ToghrJ9-X4c_hg8ijTt-Ag70WmkHOKvriZgVvbhu-9YwqBa_C1k65uulqz4TujXKakfcB5i4Z7RdGdxhrEdh2vRdgLu9UIv0Rn3eLb5bPjlNMpb44XiqXBP_TT_2TKSHZg2YRMx-jFNGqJAq8qb0PbM4dM8TODH76xV4_15bT49yTE_PuHA9jKYcC9H-rkPa-xhL6q9PFhKLEN4DX3oveYL0M-qBo8vkDCBkfeiHzUj7dIE_cEZ4erDgQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636131329,"updated":1636131329,"recoveryLevel":"Recoverable+Purgeable"}}' + headers: + cache-control: no-cache + content-length: '680' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:29 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key62211cc5/create?api-version=2016-10-01 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_0_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_0_vault.yaml new file mode 100644 index 000000000000..f441f52f0bbe --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_0_vault.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/create?api-version=7.0 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:31 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/create?api-version=7.0 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/create?api-version=7.0 + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/940432f6b7744d0fa9122d895cb89a2c","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"-fBZm71ITwyw2jgr9u422zbHE7zvY6T55146MlEwH4MFOvPxs_EdSbv25ScUGMdVP5lkn4xr64P0lQOjhI6m3nTbWvLFb81Cm5YW8Gt0YleMg_HKZgLqgdPBFDgHANLeioQCNGTkguTJ19L__8r7MhB9YuoQz0kp02-vK1ZaNNvo7lsqDMvBVcQnF-bQ0Wd-SG3oZ4Z_PRtKZMaB44WkHGbSpH8rZ2fJu8ckqSNIU6UsZokLZEy5WsDjndctrrjPFDKmMhTxePewqQ1JxL_I92F25FtW19uK9iCCsyYuBHma9_qjOCONEPNqv9GA3hDEmPqgB50jEnWbDQFzFj280Q","e":"AQAB"},"attributes":{"enabled":true,"created":1636131331,"updated":1636131331,"recoveryLevel":"Recoverable+Purgeable"}}' + headers: + cache-control: no-cache + content-length: '680' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:31 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6b91b42/create?api-version=7.0 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_1_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_1_vault.yaml new file mode 100644 index 000000000000..bae43223c033 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_1_vault.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/create?api-version=7.1 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:34 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/create?api-version=7.1 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/create?api-version=7.1 + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/885311947edd4eccbcb7265fb430acef","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"mEKr5BmEhUKeMQ1VT185F2qTEKcyu23WCX__Puze8iviw12lnQyCZ0S-tpOeR37JMJtNY40d4Yc5yCN-DEDeziWILoCrkJRVm3-LYD4p8m_ktjh-yPlG0WL1zNLYHm9G_OipsM0cUXlplEdEIA7Gl-5tCpkVr__yYv_jdDHoVyC8pLYsFkzWMvXXhFyujTUqHKjnMRR_jiMosC0Dc12LoNbM-WcXuhg-ihmomagI2pQ6_E_F1vywbhnqc-oO8KpCPuTbIKR3JnEhffCrOMORVelj6kvEQ4VuTgoY4uuocIJiCcU7UAU86HFDHjH7zkAu_H_e0Chgxp0en4cZxn4RXQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636131334,"updated":1636131334,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:34 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6c01b43/create?api-version=7.1 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_mhsm.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_mhsm.yaml new file mode 100644 index 000000000000..0ee86866acd2 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_mhsm.yaml @@ -0,0 +1,64 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/create?api-version=7.2 + response: + body: + string: '' + headers: + cache-control: no-cache + content-length: '0' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + www-authenticate: Bearer authorization="https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://managedhsm.azure.net" + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-server-latency: '1' + status: + code: 401 + message: Unauthorized + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/create?api-version=7.2 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/create?api-version=7.2 + response: + body: + string: '{"attributes":{"created":1636131337,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636131337},"key":{"e":"AQAB","key_ops":["wrapKey","decrypt","encrypt","unwrapKey","sign","verify"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/fed58eedaeb804d1b59ab2bea1dfb796","kty":"RSA-HSM","n":"4UmgdeRIG5a4n9Yb0BazkZ1x4qmn0I8oX7E8lzUG1xnKTn0NkmXRgvjUvlB8pz8074rwS89FlXD3f2r2I7viPK8xqvDSlxWOTfT-5osAQcHBbFlRUd3DIgXDYkpojroOHQq9ygERVTl6yei-Dawq_D50fETMY4Hetjqbi1AhrkA8RM_T56aFzC5hL4WqVFQTZIpMsQTRXAVp598GMl1oMn8AvUKAsPhmRwgwLY8wWOoW6kN60xJPA9cR7QrZAtb6UDJOiPj6yXu4X7ceW4tKu3iVliyKNmZxSD272sCSJqaJlezZrw96Zc8VH3arIc9b-Ku8-Tw8Raxn_o9hHixD7w"}}' + headers: + cache-control: no-cache + content-length: '741' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: westus + x-ms-server-latency: '666' + status: + code: 200 + message: OK + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key8b711acd/create?api-version=7.2 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_vault.yaml new file mode 100644 index 000000000000..669f41bd0af4 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_2_vault.yaml @@ -0,0 +1,71 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/create?api-version=7.2 + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:39 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/create?api-version=7.2 +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/create?api-version=7.2 + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/7e4059bf824742cfb3fc1bfc75fac8cb","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"yY5-Q2uVQjCdv9IJZT2DsJzkV6FolvKSu_1XN4rhN-11TMfZroiVCtBOLeeHTgPEVy9rEonytZihEZcaW2hZmZ-M1FI6WvYabMX4Lqayc8iPrZ6cRcU3ALANla2Rn6UWbrDfiZKJg4yZhJF3Aacl_CsE5NsFkkQpRiabKSU3s_df7zCxHr6xKHdZmwRvsDameyVkkGFchVxj3Ol6t37Z_MZXU24AYyH1tKNKcgcv3dpRP2g-fW91lOJsAsGk6IMLM-efKZc4iGIe6iHseNNQQE_2zBdyFOQpNwqqJV1wFTIjpLuQXpzNPIVxBebWW6RA1vq1L53ie1kr4kHK2fBcZQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636131340,"updated":1636131340,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Fri, 05 Nov 2021 16:55:40 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-keya6c71b44/create?api-version=7.2 +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_mhsm.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_mhsm.yaml new file mode 100644 index 000000000000..356c41e3422f --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_mhsm.yaml @@ -0,0 +1,120 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/create?api-version=7.3-preview + response: + body: + string: '' + headers: + cache-control: no-cache + content-length: '0' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + www-authenticate: Bearer authorization="https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://managedhsm.azure.net" + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-server-latency: '1' + status: + code: 401 + message: Unauthorized + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/create?api-version=7.3-preview +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/create?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636416230,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636416230},"key":{"e":"AQAB","key_ops":["wrapKey","sign","verify","encrypt","decrypt","unwrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/b9455b5cd9a34d3b2c3a637e61a550f9","kty":"RSA-HSM","n":"r5y21Ndv_xZgYFpqLHejepFyjnoBIQQ8usaUK0d4K6YyEYVRb2MbokiZqvKoOQ3jcvpOfoFlikEYi207ykpi8ukRjiY-vD_MjjkN_iJRASZZ7Z_jP5NsqGanHLIwQGA5orTH_rRxNeGBmRx6lCxnUT2CKKOOTDEorQK9H1FhaWpekKhdfJDUOVKRy-SER46fRDd7lWwqAQ5uUJ9nP0JM1vF8zeaYXlU7b7TmH4dQsTFeEY9TcRJPCGEnkadHk17l6xHK17DTg9YrVN_T6TcALQLdevVb5wefIF6yHin_1_hwjcTG6WxDorZzP-XVlHplGAXoW_KvJZ_akW-chShkXQ"}}' + headers: + cache-control: no-cache + content-length: '741' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: westus + x-ms-server-latency: '376' + status: + code: 200 + message: OK + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/create?api-version=7.3-preview +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636416230,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636416230},"key":{"e":"AQAB","key_ops":["unwrapKey","decrypt","encrypt","verify","sign","wrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/b9455b5cd9a34d3b2c3a637e61a550f9","kty":"RSA-HSM","n":"r5y21Ndv_xZgYFpqLHejepFyjnoBIQQ8usaUK0d4K6YyEYVRb2MbokiZqvKoOQ3jcvpOfoFlikEYi207ykpi8ukRjiY-vD_MjjkN_iJRASZZ7Z_jP5NsqGanHLIwQGA5orTH_rRxNeGBmRx6lCxnUT2CKKOOTDEorQK9H1FhaWpekKhdfJDUOVKRy-SER46fRDd7lWwqAQ5uUJ9nP0JM1vF8zeaYXlU7b7TmH4dQsTFeEY9TcRJPCGEnkadHk17l6xHK17DTg9YrVN_T6TcALQLdevVb5wefIF6yHin_1_hwjcTG6WxDorZzP-XVlHplGAXoW_KvJZ_akW-chShkXQ"}}' + headers: + cache-control: no-cache + content-length: '741' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-build-version: 1.0.20210929-1-5b78c022-develop + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: westus + x-ms-server-latency: '92' + status: + code: 200 + message: OK + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/?api-version=7.3-preview +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/?api-version=7.3-preview + response: + body: + string: '{"attributes":{"created":1636416230,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1636416230},"key":{"e":"AQAB","key_ops":["unwrapKey","decrypt","encrypt","verify","sign","wrapKey"],"kid":"https://managedhsmname.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/b9455b5cd9a34d3b2c3a637e61a550f9","kty":"RSA-HSM","n":"r5y21Ndv_xZgYFpqLHejepFyjnoBIQQ8usaUK0d4K6YyEYVRb2MbokiZqvKoOQ3jcvpOfoFlikEYi207ykpi8ukRjiY-vD_MjjkN_iJRASZZ7Z_jP5NsqGanHLIwQGA5orTH_rRxNeGBmRx6lCxnUT2CKKOOTDEorQK9H1FhaWpekKhdfJDUOVKRy-SER46fRDd7lWwqAQ5uUJ9nP0JM1vF8zeaYXlU7b7TmH4dQsTFeEY9TcRJPCGEnkadHk17l6xHK17DTg9YrVN_T6TcALQLdevVb5wefIF6yHin_1_hwjcTG6WxDorZzP-XVlHplGAXoW_KvJZ_akW-chShkXQ"}}' + headers: + cache-control: no-cache + content-length: '741' + content-security-policy: default-src 'self' + content-type: application/json; charset=utf-8 + strict-transport-security: max-age=31536000; includeSubDomains + x-content-type-options: nosniff + x-frame-options: SAMEORIGIN + x-ms-build-version: 1.0.20210929-1-5b78c022-develop + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=Ipv4; + x-ms-keyvault-region: westus + x-ms-server-latency: '0' + status: + code: 200 + message: OK + url: https://mcpatinotesthsm.managedhsm.azure.net/keys/livekvtestmultitenant-key713c1e2f/?api-version=7.3-preview +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_vault.yaml b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_vault.yaml new file mode 100644 index 000000000000..840be1178954 --- /dev/null +++ b/sdk/keyvault/azure-keyvault-keys/tests/recordings/test_challenge_auth_async.test_multitenant_authentication_7_3_preview_vault.yaml @@ -0,0 +1,129 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Content-Length: + - '0' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/create?api-version=7.3-preview + response: + body: + string: '{"error":{"code":"Unauthorized","message":"AKV10000: Request is missing + a Bearer or PoP token."}}' + headers: + cache-control: no-cache + content-length: '97' + content-type: application/json; charset=utf-8 + date: Tue, 09 Nov 2021 00:03:53 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + www-authenticate: Bearer authorization="https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47", + resource="https://vault.azure.net" + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 401 + message: Unauthorized + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/create?api-version=7.3-preview +- request: + body: '{"kty": "RSA"}' + headers: + Accept: + - application/json + Content-Length: + - '14' + Content-Type: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: POST + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/create?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/f875421ae9684ee89ffa78d33c4b917c","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"uh7LV8qFwSDSazU05d4aArOVG7HK72oX8Qvdlq-5FJwPRbbCTU9ZX7BJcI7dnhvQHHpYnvZ54aHaNX4nrqYRJRKv9fgte_ZyGaaBqnUVd_sjf3f8US7O6ayPzhzV-F8x2ML12yeRuXbsquxG05D3_e7OtAm5aSwqHxPoIYKIjh4hI3FtpryM7ECA3op1M9QH4e-_3CGCSbLEomih0_FZ-WkebiHNryYJi2l02p-b-CNAEYlcRpRlZUm8Gt6ZtKJ-YvaT1uvPw83DSW6Q_LDjhojmswJBUYCtWszDjBbcUsi5dKD0Zo7--ANcC-66H-CnTM5cF5RRygj1N5l2DU8xyQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636416233,"updated":1636416233,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Tue, 09 Nov 2021 00:03:53 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/create?api-version=7.3-preview +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/f875421ae9684ee89ffa78d33c4b917c","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"uh7LV8qFwSDSazU05d4aArOVG7HK72oX8Qvdlq-5FJwPRbbCTU9ZX7BJcI7dnhvQHHpYnvZ54aHaNX4nrqYRJRKv9fgte_ZyGaaBqnUVd_sjf3f8US7O6ayPzhzV-F8x2ML12yeRuXbsquxG05D3_e7OtAm5aSwqHxPoIYKIjh4hI3FtpryM7ECA3op1M9QH4e-_3CGCSbLEomih0_FZ-WkebiHNryYJi2l02p-b-CNAEYlcRpRlZUm8Gt6ZtKJ-YvaT1uvPw83DSW6Q_LDjhojmswJBUYCtWszDjBbcUsi5dKD0Zo7--ANcC-66H-CnTM5cF5RRygj1N5l2DU8xyQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636416233,"updated":1636416233,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Tue, 09 Nov 2021 00:03:54 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/?api-version=7.3-preview +- request: + body: null + headers: + Accept: + - application/json + User-Agent: + - azsdk-python-keyvault-keys/4.5.0b5 Python/3.10.0 (Windows-10-10.0.22000-SP0) + method: GET + uri: https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/?api-version=7.3-preview + response: + body: + string: '{"key":{"kid":"https://vaultname.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/f875421ae9684ee89ffa78d33c4b917c","kty":"RSA","key_ops":["encrypt","decrypt","sign","verify","wrapKey","unwrapKey"],"n":"uh7LV8qFwSDSazU05d4aArOVG7HK72oX8Qvdlq-5FJwPRbbCTU9ZX7BJcI7dnhvQHHpYnvZ54aHaNX4nrqYRJRKv9fgte_ZyGaaBqnUVd_sjf3f8US7O6ayPzhzV-F8x2ML12yeRuXbsquxG05D3_e7OtAm5aSwqHxPoIYKIjh4hI3FtpryM7ECA3op1M9QH4e-_3CGCSbLEomih0_FZ-WkebiHNryYJi2l02p-b-CNAEYlcRpRlZUm8Gt6ZtKJ-YvaT1uvPw83DSW6Q_LDjhojmswJBUYCtWszDjBbcUsi5dKD0Zo7--ANcC-66H-CnTM5cF5RRygj1N5l2DU8xyQ","e":"AQAB"},"attributes":{"enabled":true,"created":1636416233,"updated":1636416233,"recoveryLevel":"Recoverable+Purgeable","recoverableDays":90}}' + headers: + cache-control: no-cache + content-length: '701' + content-type: application/json; charset=utf-8 + date: Tue, 09 Nov 2021 00:03:54 GMT + expires: '-1' + pragma: no-cache + strict-transport-security: max-age=31536000;includeSubDomains + x-content-type-options: nosniff + x-ms-keyvault-network-info: conn_type=Ipv4;addr=172.92.159.124;act_addr_fam=InterNetwork; + x-ms-keyvault-region: westus + x-ms-keyvault-service-version: 1.9.150.1 + x-powered-by: ASP.NET + status: + code: 200 + message: OK + url: https://mcpatinotest.vault.azure.net/keys/livekvtestmultitenant-key8ff41ea6/?api-version=7.3-preview +version: 1 diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py index d4f1eff32af8..955141d98943 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py @@ -7,6 +7,7 @@ the challenge cache is global to the process. """ import functools +import os import time from uuid import uuid4 @@ -20,11 +21,53 @@ from azure.core.pipeline import Pipeline from azure.core.pipeline.policies import SansIOHTTPPolicy from azure.core.pipeline.transport import HttpRequest +from azure.identity import ClientSecretCredential +from azure.keyvault.keys import KeyClient from azure.keyvault.keys._shared import ChallengeAuthPolicy, HttpChallenge, HttpChallengeCache +from azure.keyvault.keys._shared.client_base import DEFAULT_VERSION import pytest from _shared.helpers import mock_response, Request, validating_transport +from _shared.test_case import KeyVaultTestCase +from _test_case import client_setup, get_decorator, KeysTestCase + + +only_default_version = get_decorator(api_versions=[DEFAULT_VERSION]) + + +class ChallengeAuthTests(KeysTestCase, KeyVaultTestCase): + def __init__(self, *args, **kwargs): + super(ChallengeAuthTests, self).__init__(*args, match_body=False, **kwargs) + + @only_default_version() + @client_setup + def test_multitenant_authentication(self, client, is_hsm, **kwargs): + if not self.is_live: + pytest.skip("This test is incompatible with vcrpy in playback") + + client_id = os.environ.get("KEYVAULT_CLIENT_ID") + client_secret = os.environ.get("KEYVAULT_CLIENT_SECRET") + if not (client_id and client_secret): + pytest.skip("Values for KEYVAULT_CLIENT_ID and KEYVAULT_CLIENT_SECRET are required") + + # we set up a client for this method to align with the async test, but we actually want to create a new client + # this new client should use a credential with an initially fake tenant ID and still succeed with a real request + credential = ClientSecretCredential(tenant_id=str(uuid4()), client_id=client_id, client_secret=client_secret) + vault_url = self.managed_hsm_url if is_hsm else self.vault_url + client = KeyClient(vault_url=vault_url, credential=credential) + + if self.is_live: + time.sleep(2) # to avoid throttling by the service + key_name = self.get_resource_name("multitenant-key") + key = client.create_rsa_key(key_name) + assert key.id + + # try making another request with the credential's token revoked + # the challenge policy should correctly request a new token for the correct tenant when a challenge is cached + client._client._config.authentication_policy._token = None + fetched_key = client.get_key(key_name) + assert key.id == fetched_key.id def empty_challenge_cache(fn): @@ -74,7 +117,8 @@ def test_challenge_cache(): def test_challenge_parsing(): - authority = "https://login.authority.net/tenant" + tenant = "tenant" + authority = "https://login.authority.net/{}".format(tenant) resource = "https://challenge.resource" challenge = HttpChallenge( "https://request.uri", challenge="Bearer authorization={}, resource={}".format(authority, resource) @@ -82,6 +126,7 @@ def test_challenge_parsing(): assert challenge.get_authorization_server() == authority assert challenge.get_resource() == resource + assert challenge.tenant_id == tenant @empty_challenge_cache @@ -111,7 +156,7 @@ def send(request): return Mock(status_code=200) raise ValueError("unexpected request") - def get_token(*scopes): + def get_token(*scopes, **_): assert len(scopes) == 1 assert scopes[0] == expected_scope return AccessToken(expected_token, 0) @@ -143,6 +188,56 @@ def get_token(*scopes): test_with_challenge(challenge_with_scope, scope) +@empty_challenge_cache +def test_tenant(): + """The policy's token requests should pass the parsed tenant ID from the challenge""" + + expected_content = b"a duck" + + def test_with_challenge(challenge, expected_tenant): + expected_token = "expected_token" + + class Requests: + count = 0 + + def send(request): + Requests.count += 1 + if Requests.count == 1: + # first request should be unauthorized and have no content + assert not request.body + assert request.headers["Content-Length"] == "0" + return challenge + elif Requests.count == 2: + # second request should be authorized according to challenge and have the expected content + assert request.headers["Content-Length"] + assert request.body == expected_content + assert expected_token in request.headers["Authorization"] + return Mock(status_code=200) + raise ValueError("unexpected request") + + def get_token(*_, **kwargs): + assert kwargs.get("tenant_id") == expected_tenant + return AccessToken(expected_token, 0) + + credential = Mock(get_token=Mock(wraps=get_token)) + pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=Mock(send=send)) + request = HttpRequest("POST", get_random_url()) + request.set_bytes_body(expected_content) + pipeline.run(request) + + assert credential.get_token.call_count == 1 + + tenant = "tenant-id" + endpoint = "https://authority.net/{}".format(tenant) + + challenge = Mock( + status_code=401, + headers={"WWW-Authenticate": 'Bearer authorization="{}", resource=https://challenge.resource'.format(endpoint)}, + ) + + test_with_challenge(challenge, tenant) + + @empty_challenge_cache def test_policy_updates_cache(): """ @@ -242,11 +337,7 @@ def get_token(*_, **__): @empty_challenge_cache def test_preserves_options_and_headers(): - """After a challenge, the original request should be sent with its options and headers preserved. - - If a policy mutates the options or headers of the challenge (unauthorized) request, the options of the service - request should be present when it is sent with authorization. - """ + """After a challenge, the policy should send the original request with its options and headers preserved""" url = get_random_url() token = "**" @@ -265,51 +356,31 @@ def get_token(*_, **__): ] + [mock_response()] * 2, ) - challenge_policy = ChallengeAuthPolicy(credential=credential) - policies = get_policies_for_request_mutation_test(challenge_policy) - pipeline = Pipeline(policies=policies, transport=transport) - - response = pipeline.run(HttpRequest("GET", url)) - - # ensure the mock sans I/O policies were called - for policy in policies: - if hasattr(policy, "on_request"): - assert policy.on_request.called, "mock policy wasn't invoked" - -def get_policies_for_request_mutation_test(challenge_policy): - # create mock policies to add, remove, and verify an option and header key = "foo" value = "bar" - do_not_handle = lambda _: False def add(request): # add the expected option and header request.context.options[key] = value request.http_request.headers[key] = value - adder = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=add), on_exception=do_not_handle) - - def remove(request): - # remove expected header and all options of unauthorized (challenge) requests - if not request.http_request.headers.get("Authorization"): - request.http_request.headers.pop(key, None) - request.context.options = {} - - remover = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=remove), on_exception=do_not_handle) + adder = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=add), on_exception=lambda _: False) def verify(request): # authorized (non-challenge) requests should have the expected option and header if request.http_request.headers.get("Authorization"): - assert request.context.options.get(key) == value, "request option not preserved across challenge" - assert request.http_request.headers.get(key) == value, "headers not preserved across challenge" + assert request.context.options.get(key) == value, "request option wasn't preserved across challenge" + assert request.http_request.headers.get(key) == value, "headers wasn't preserved across challenge" verifier = Mock(spec=SansIOHTTPPolicy, on_request=Mock(wraps=verify)) - # Mutating the challenge request shouldn't affect the authorized request. - # This is the pipeline flow: - # 1. add option and header - # 2. challenge auth - # 3. remove option, header from unauthorized request - # 4. verify option, header on authorized request - return [adder, challenge_policy, remover, verifier] + challenge_policy = ChallengeAuthPolicy(credential=credential) + policies = [adder, challenge_policy, verifier] + pipeline = Pipeline(policies=policies, transport=transport) + + pipeline.run(HttpRequest("GET", url)) + + # ensure the mock sans I/O policies were called + assert adder.on_request.called, "mock policy wasn't invoked" + assert verifier.on_request.called, "mock policy wasn't invoked" diff --git a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py index cf00e192ffa2..0ef669809548 100644 --- a/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py +++ b/sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth_async.py @@ -6,23 +6,66 @@ Tests for the HTTP challenge authentication implementation. These tests aren't parallelizable, because the challenge cache is global to the process. """ +import asyncio +import os import time +from uuid import uuid4 -try: - from unittest.mock import Mock, patch -except ImportError: # python < 3.3 - from mock import Mock, patch # type: ignore +from unittest.mock import Mock, patch from azure.core.credentials import AccessToken from azure.core.exceptions import ServiceRequestError from azure.core.pipeline import AsyncPipeline +from azure.core.pipeline.policies import SansIOHTTPPolicy from azure.core.pipeline.transport import HttpRequest +from azure.identity.aio import ClientSecretCredential +from azure.keyvault.keys.aio import KeyClient from azure.keyvault.keys._shared import AsyncChallengeAuthPolicy, HttpChallenge, HttpChallengeCache +from azure.keyvault.keys._shared.client_base import DEFAULT_VERSION import pytest from _shared.helpers import mock_response, Request from _shared.helpers_async import async_validating_transport -from test_challenge_auth import empty_challenge_cache, get_policies_for_request_mutation_test, get_random_url +from _shared.test_case_async import KeyVaultTestCase +from _test_case import client_setup, get_decorator, KeysTestCase +from test_challenge_auth import empty_challenge_cache, get_random_url + + +only_default_version = get_decorator(is_async=True, api_versions=[DEFAULT_VERSION]) + + +class ChallengeAuthTests(KeysTestCase, KeyVaultTestCase): + def __init__(self, *args, **kwargs): + super().__init__(*args, match_body=False, **kwargs) + + @only_default_version() + @client_setup + async def test_multitenant_authentication(self, client, is_hsm, **kwargs): + if not self.is_live: + pytest.skip("This test is incompatible with vcrpy in playback") + + client_id = os.environ.get("KEYVAULT_CLIENT_ID") + client_secret = os.environ.get("KEYVAULT_CLIENT_SECRET") + if not (client_id and client_secret): + pytest.skip("Values for KEYVAULT_CLIENT_ID and KEYVAULT_CLIENT_SECRET are required") + + # we set up a client for this method so it gets awaited, but we actually want to create a new client + # this new client should use a credential with an initially fake tenant ID and still succeed with a real request + credential = ClientSecretCredential(tenant_id=str(uuid4()), client_id=client_id, client_secret=client_secret) + vault_url = self.managed_hsm_url if is_hsm else self.vault_url + client = KeyClient(vault_url=vault_url, credential=credential) + + if self.is_live: + await asyncio.sleep(2) # to avoid throttling by the service + key_name = self.get_resource_name("multitenant-key") + key = await client.create_rsa_key(key_name) + assert key.id + + # try making another request with the credential's token revoked + # the challenge policy should correctly request a new token for the correct tenant when a challenge is cached + client._client._config.authentication_policy._token = None + fetched_key = await client.get_key(key_name) + assert key.id == fetched_key.id @pytest.mark.asyncio @@ -65,13 +108,15 @@ async def send(request): return Mock(status_code=200) raise ValueError("unexpected request") - async def get_token(*scopes): + async def get_token(*scopes, **_): assert len(scopes) == 1 assert scopes[0] == expected_scope return AccessToken(expected_token, 0) credential = Mock(get_token=Mock(wraps=get_token)) - pipeline = AsyncPipeline(policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=Mock(send=send)) + pipeline = AsyncPipeline( + policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=Mock(send=send) + ) request = HttpRequest("POST", get_random_url()) request.set_bytes_body(expected_content) await pipeline.run(request) @@ -97,6 +142,59 @@ async def get_token(*scopes): await test_with_challenge(challenge_with_scope, scope) +@pytest.mark.asyncio +@empty_challenge_cache +async def test_tenant(): + """The policy's token requests should pass the parsed tenant ID from the challenge""" + + expected_content = b"a duck" + + async def test_with_challenge(challenge, expected_tenant): + expected_token = "expected_token" + + class Requests: + count = 0 + + async def send(request): + Requests.count += 1 + if Requests.count == 1: + # first request should be unauthorized and have no content + assert not request.body + assert request.headers["Content-Length"] == "0" + return challenge + elif Requests.count == 2: + # second request should be authorized according to challenge and have the expected content + assert request.headers["Content-Length"] + assert request.body == expected_content + assert expected_token in request.headers["Authorization"] + return Mock(status_code=200) + raise ValueError("unexpected request") + + async def get_token(*_, **kwargs): + assert kwargs.get("tenant_id") == expected_tenant + return AccessToken(expected_token, 0) + + credential = Mock(get_token=Mock(wraps=get_token)) + pipeline = AsyncPipeline( + policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=Mock(send=send) + ) + request = HttpRequest("POST", get_random_url()) + request.set_bytes_body(expected_content) + await pipeline.run(request) + + assert credential.get_token.call_count == 1 + + tenant = "tenant-id" + endpoint = "https://authority.net/{}".format(tenant) + + challenge = Mock( + status_code=401, + headers={"WWW-Authenticate": 'Bearer authorization="{}", resource=https://challenge.resource'.format(endpoint)}, + ) + + await test_with_challenge(challenge, tenant) + + @pytest.mark.asyncio @empty_challenge_cache async def test_policy_updates_cache(): @@ -204,11 +302,7 @@ async def get_token(*_, **__): @pytest.mark.asyncio @empty_challenge_cache async def test_preserves_options_and_headers(): - """After a challenge, the original request should be sent with its options and headers preserved. - - If a policy mutates the options or headers of the challenge (unauthorized) request, the options of the service - request should be present when it is sent with authorization. - """ + """After a challenge, the policy should send the original request with its options and headers preserved""" url = get_random_url() @@ -228,13 +322,30 @@ async def get_token(*_, **__): ] + [mock_response()] * 2, ) + key = "foo" + value = "bar" + + def add(request): + # add the expected option and header + request.context.options[key] = value + request.http_request.headers[key] = value + + adder = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock(wraps=add), on_exception=lambda _: False) + + def verify(request): + # authorized (non-challenge) requests should have the expected option and header + if request.http_request.headers.get("Authorization"): + assert request.context.options.get(key) == value, "request option wasn't preserved across challenge" + assert request.http_request.headers.get(key) == value, "headers wasn't preserved across challenge" + + verifier = Mock(spec=SansIOHTTPPolicy, on_request=Mock(wraps=verify)) + challenge_policy = AsyncChallengeAuthPolicy(credential=credential) - policies = get_policies_for_request_mutation_test(challenge_policy) + policies = [adder, challenge_policy, verifier] pipeline = AsyncPipeline(policies=policies, transport=transport) - response = await pipeline.run(HttpRequest("GET", url)) + await pipeline.run(HttpRequest("GET", url)) - # ensure the mock sans I/O policies were used - for policy in policies: - if hasattr(policy, "on_request"): - assert policy.on_request.called, "mock policy wasn't invoked" + # ensure the mock sans I/O policies were called + assert adder.on_request.called, "mock policy wasn't invoked" + assert verifier.on_request.called, "mock policy wasn't invoked" diff --git a/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md b/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md index 679cdf8ae1ba..c7858f5e7165 100644 --- a/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md +++ b/sdk/keyvault/azure-keyvault-secrets/CHANGELOG.md @@ -3,12 +3,15 @@ ## 4.4.0b2 (Unreleased) ### Features Added +- Added support for multi-tenant authentication when using `azure-identity` 1.7.1 or newer + ([#20698](https://github.com/Azure/azure-sdk-for-python/issues/20698)) ### Breaking Changes ### Bugs Fixed ### Other Changes +- Updated minimum `azure-core` version to 1.15.0 ## 4.4.0b1 (2021-09-09) diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py index 3329ce2df068..d8303b4b081d 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/__init__.py @@ -9,7 +9,7 @@ import urlparse as parse # type: ignore from typing import TYPE_CHECKING -from .challenge_auth_policy import ChallengeAuthPolicy, ChallengeAuthPolicyBase +from .challenge_auth_policy import ChallengeAuthPolicy from .client_base import KeyVaultClientBase from .http_challenge import HttpChallenge from . import http_challenge_cache as HttpChallengeCache @@ -21,7 +21,6 @@ __all__ = [ "ChallengeAuthPolicy", - "ChallengeAuthPolicyBase", "HttpChallenge", "HttpChallengeCache", "KeyVaultClientBase", diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/async_challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/async_challenge_auth_policy.py index 97f1d093e20f..bfe46689f5ff 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/async_challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/async_challenge_auth_policy.py @@ -13,67 +13,69 @@ requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the protocol again. """ + +import time from typing import TYPE_CHECKING -from azure.core.pipeline.policies import AsyncHTTPPolicy +from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy -from . import HttpChallengeCache -from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase +from . import http_challenge_cache as ChallengeCache +from .challenge_auth_policy import _enforce_tls, _update_challenge if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional + from azure.core.credentials import AccessToken from azure.core.credentials_async import AsyncTokenCredential - from azure.core.pipeline import PipelineRequest - from azure.core.pipeline.transport import AsyncHttpResponse - from . import HttpChallenge + from azure.core.pipeline import PipelineRequest, PipelineResponse -class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): +class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: + def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: + super().__init__(credential, *scopes, **kwargs) self._credential = credential - super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": + async def on_request(self, request: "PipelineRequest") -> None: _enforce_tls(request) - - challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = await self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - await self._handle_challenge(request, challenge) - response = await self.next.send(request) - - return response - - async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + + async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = await self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) + + return True - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + @property + def _need_new_token(self) -> bool: + # pylint:disable=invalid-overridden-method + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/challenge_auth_policy.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/challenge_auth_policy.py index 3239032e9162..7f6f2b93f0e4 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/challenge_auth_policy.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/challenge_auth_policy.py @@ -14,13 +14,11 @@ protocol again. """ -import copy import time from azure.core.exceptions import ServiceRequestError -from azure.core.pipeline import PipelineContext, PipelineRequest -from azure.core.pipeline.policies import HTTPPolicy -from azure.core.pipeline.transport import HttpRequest +from azure.core.pipeline import PipelineRequest +from azure.core.pipeline.policies import BearerTokenCredentialPolicy from .http_challenge import HttpChallenge from . import http_challenge_cache as ChallengeCache @@ -44,22 +42,6 @@ def _enforce_tls(request): ) -def _get_challenge_request(request): - # type: (PipelineRequest) -> PipelineRequest - - # The challenge request is intended to provoke an authentication challenge from Key Vault, to learn how the - # service request should be authenticated. It should be identical to the service request but with no body. - challenge_request = HttpRequest( - request.http_request.method, request.http_request.url, headers=request.http_request.headers - ) - challenge_request.headers["Content-Length"] = "0" - - options = copy.deepcopy(request.context.options) - context = PipelineContext(request.context.transport, **options) - - return PipelineRequest(http_request=challenge_request, context=context) - - def _update_challenge(request, challenger): # type: (PipelineRequest, PipelineResponse) -> HttpChallenge """parse challenge from challenger, cache it, return it""" @@ -73,68 +55,55 @@ def _update_challenge(request, challenger): return challenge -class ChallengeAuthPolicyBase(object): - """Sans I/O base for challenge authentication policies""" - - def __init__(self, **kwargs): - self._token = None # type: Optional[AccessToken] - super(ChallengeAuthPolicyBase, self).__init__(**kwargs) - - @property - def _need_new_token(self): - # type: () -> bool - return not self._token or self._token.expires_on - time.time() < 300 - - -class ChallengeAuthPolicy(ChallengeAuthPolicyBase, HTTPPolicy): +class ChallengeAuthPolicy(BearerTokenCredentialPolicy): """policy for handling HTTP authentication challenges""" - def __init__(self, credential, **kwargs): - # type: (TokenCredential, **Any) -> None + def __init__(self, credential, *scopes, **kwargs): + # type: (TokenCredential, *str, **Any) -> None + super(ChallengeAuthPolicy, self).__init__(credential, *scopes, **kwargs) self._credential = credential - super(ChallengeAuthPolicy, self).__init__(**kwargs) + self._token = None # type: Optional[AccessToken] - def send(self, request): - # type: (PipelineRequest) -> PipelineResponse + def on_request(self, request): + # type: (PipelineRequest) -> None _enforce_tls(request) - challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) - if not challenge: - challenge_request = _get_challenge_request(request) - challenger = self.next.send(challenge_request) - try: - challenge = _update_challenge(request, challenger) - except ValueError: - # didn't receive the expected challenge -> nothing more this policy can do - return challenger - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - if response.http_response.status_code == 401: - # any cached token must be invalid - self._token = None - - # cached challenge could be outdated; maybe this response has a new one? - try: - challenge = _update_challenge(request, response) - except ValueError: - # 401 with no legible challenge -> nothing more this policy can do - return response - - self._handle_challenge(request, challenge) - response = self.next.send(request) - - return response - - def _handle_challenge(self, request, challenge): - # type: (PipelineRequest, HttpChallenge) -> None - """authenticate according to challenge, add Authorization header to request""" - - if self._need_new_token: + if challenge: + # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. + if self._need_new_token: + # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource + scope = challenge.get_scope() or challenge.get_resource() + "/.default" + self._token = self._credential.get_token(scope, tenant_id=challenge.tenant_id) + + # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token + request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return + + # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, + # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. + # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell + # super to send it again. + if request.http_request.body: + request.context["key_vault_request_data"] = request.http_request.body + request.http_request.set_json_body(None) + request.http_request.headers["Content-Length"] = "0" + + def on_challenge(self, request, response): + # type: (PipelineRequest, PipelineResponse) -> bool + try: + challenge = _update_challenge(request, response) # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource scope = challenge.get_scope() or challenge.get_resource() + "/.default" - self._token = self._credential.get_token(scope) + except ValueError: + return False + + body = request.context.pop("key_vault_request_data", None) + request.http_request.set_text_body(body) # no-op when text is None + self.authorize_request(request, scope, tenant_id=challenge.tenant_id) - # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token - request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore + return True + + @property + def _need_new_token(self): + # type: () -> bool + return not self._token or self._token.expires_on - time.time() < 300 diff --git a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge.py b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge.py index c762e1ae50ef..c52c90929ad9 100644 --- a/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge.py +++ b/sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge.py @@ -40,6 +40,11 @@ def __init__(self, request_uri, challenge, response_headers=None): if "authorization" not in self._parameters and "authorization_uri" not in self._parameters: raise ValueError("Invalid challenge parameters") + authorization_uri = self.get_authorization_server() + # the authoritzation server URI should look something like https://login.windows.net/tenant-id + uri_path = parse.urlparse(authorization_uri).path.lstrip("/") + self.tenant_id = uri_path.split("/")[0] or None + # if the response headers were supplied if response_headers: # get the message signing key and message key encryption key from the headers diff --git a/sdk/keyvault/azure-keyvault-secrets/setup.py b/sdk/keyvault/azure-keyvault-secrets/setup.py index b8e377b75496..d74810b67dbf 100644 --- a/sdk/keyvault/azure-keyvault-secrets/setup.py +++ b/sdk/keyvault/azure-keyvault-secrets/setup.py @@ -83,7 +83,7 @@ ] ), install_requires=[ - "azure-core<2.0.0,>=1.7.0", + "azure-core<2.0.0,>=1.15.0", "msrest>=0.6.21", "azure-common~=1.1", ], diff --git a/shared_requirements.txt b/shared_requirements.txt index 1a7676337da6..ff41986ef306 100644 --- a/shared_requirements.txt +++ b/shared_requirements.txt @@ -140,6 +140,10 @@ backports.functools-lru-cache >= 1.6.4; python_version == "2.7" #override azure-data-tables msrest>=0.6.21 #override azure-eventhub azure-core<2.0.0,>=1.14.0 #override azure-identity azure-core<2.0.0,>=1.11.0 +#override azure-keyvault-administration azure-core<2.0.0,>=1.15.0 +#override azure-keyvault-certificates azure-core<2.0.0,>=1.15.0 +#override azure-keyvault-keys azure-core<2.0.0,>=1.15.0 +#override azure-keyvault-secrets azure-core<2.0.0,>=1.15.0 #override azure-identity cryptography>=2.5 #override azure-keyvault-administration msrest>=0.6.21 #override azure-keyvault-administration azure-core<2.0.0,>=1.11.0