Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sdk/keyvault/azure-keyvault-administration/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
## 4.1.0b2 (Unreleased)

### Features Added
- Added support for multi-tenant authentication against Managed HSM 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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@

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


__all__ = [
"ChallengeAuthPolicy",
"ChallengeAuthPolicyBase",
"HttpChallenge",
"HttpChallengeCache",
"KeyVaultClientBase",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,65 +15,59 @@
"""
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 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
from .http_challenge import HttpChallenge


class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy):
class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give more context about this change?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just updated the PR description with more context!

"""policy for handling HTTP authentication challenges"""

def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None:
self._credential = credential
super(AsyncChallengeAuthPolicy, self).__init__(**kwargs)

async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse":
async def on_request(self, request: "PipelineRequest") -> None:
_enforce_tls(request)
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.
await self._handle_challenge(request, challenge)
Comment thread
laiapat marked this conversation as resolved.
Outdated
return

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
# 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"

await self._handle_challenge(request, challenge)
response = await self.next.send(request)

return response
async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool:
try:
challenge = _update_challenge(request, response)
scope = challenge.get_scope() or challenge.get_resource() + "/.default"
except ValueError:
return False

self._scopes = (scope,)
Comment thread
laiapat marked this conversation as resolved.
Outdated

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, *self._scopes, tenant_id=challenge.tenant_id)

return True

async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None:
"""authenticate according to challenge, add Authorization header to request"""
"""Authenticate according to challenge, add Authorization header to request"""

if self._need_new_token:
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)
self._token = await self._credential.get_token(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
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,9 @@
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
Expand All @@ -31,8 +27,6 @@
TYPE_CHECKING = False

if TYPE_CHECKING:
from typing import Any, Optional
from azure.core.credentials import AccessToken, TokenCredential
from azure.core.pipeline import PipelineResponse


Expand All @@ -44,22 +38,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"""
Expand All @@ -73,68 +51,51 @@ 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
self._credential = credential
super(ChallengeAuthPolicy, self).__init__(**kwargs)

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

if challenge:
# Note that if the vault has moved to a new tenant since our last request for it, this request will fail.
self._handle_challenge(request, challenge)
response = self.next.send(request)
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)
scope = challenge.get_scope() or challenge.get_resource() + "/.default"
except ValueError:
return False

self._scopes = (scope,)

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, *self._scopes, tenant_id=challenge.tenant_id)

return response
return True

def _handle_challenge(self, request, challenge):
# type: (PipelineRequest, HttpChallenge) -> None
"""authenticate according to challenge, add Authorization header to request"""
"""Authenticate according to challenge, add Authorization header to request"""

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)
self._token = self._credential.get_token(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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion sdk/keyvault/azure-keyvault-administration/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
4 changes: 4 additions & 0 deletions sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
## 4.4.0b2 (Unreleased)

### Features Added
- Added support for multi-tenant authentication against Managed HSM when using
Comment thread
laiapat marked this conversation as resolved.
Outdated
`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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,7 +21,6 @@

__all__ = [
"ChallengeAuthPolicy",
"ChallengeAuthPolicyBase",
"HttpChallenge",
"HttpChallengeCache",
"KeyVaultClientBase",
Expand Down
Loading