-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[Identity] Implement binding mode support in WorkloadIdentityCredential #43287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
ab9618c
[Identity] Implement binding mode support in WorkloadIdentityCredential
pvaneck 747f53d
Refactor http.client transport and add aio transport
pvaneck c62e4b8
Updates
pvaneck 0e08bbf
Update changelog
pvaneck 321d545
Use RequestsTransports instead
pvaneck 23824fa
Refactor
pvaneck 2071d34
Update changelog
pvaneck aec1604
test updates
pvaneck 6d0ec71
Updates
pvaneck 93ae934
kwarg update
pvaneck d3695f7
Merge branch 'main' into identity-aks-fic
pvaneck 92182b4
Updates
pvaneck 088f46e
Clean up tests
pvaneck 164cd5e
Merge branch 'main' into identity-aks-fic
pvaneck File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
sdk/identity/azure-identity/azure/identity/_internal/token_binding_transport_mixin.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # ------------------------------------ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
| # ------------------------------------ | ||
| # cspell:ignore cafile | ||
| import os | ||
| import urllib.parse | ||
| from typing import Optional, Any | ||
|
|
||
| from azure.core.rest import HttpRequest | ||
|
|
||
|
|
||
| class TokenBindingTransportMixin: | ||
| """Mixin class providing URL validation, CA file tracking, and proxy URL functionality for transport classes.""" | ||
|
|
||
| def __init__(self, **kwargs: Any) -> None: | ||
| """Initialize CA file tracking and proxy attributes.""" | ||
| self._ca_file = kwargs.pop("ca_file", None) | ||
| self._ca_data = kwargs.pop("ca_data", None) | ||
| self._proxy_endpoint = kwargs.pop("proxy_endpoint", None) | ||
| self._sni = kwargs.pop("sni", None) | ||
|
|
||
| self._ca_file_mtime: Optional[float] = None | ||
|
|
||
| if self._ca_file and self._ca_data: | ||
| raise ValueError("Both ca_file and ca_data are set. Only one should be set") | ||
|
|
||
| if self._proxy_endpoint: | ||
| self._validate_url(self._proxy_endpoint) | ||
|
|
||
| # If we have a ca_file, read it once and store as ca_data | ||
| if self._ca_file: | ||
| self._load_ca_file_to_data() | ||
|
|
||
| super().__init__() | ||
|
|
||
| def _validate_url(self, url: str) -> None: | ||
| """Validate that a URL meets security requirements for HTTPS connections. | ||
|
|
||
| :param url: The URL to validate. | ||
| :type url: str | ||
| :raises ValueError: If the URL does not meet security requirements. | ||
| """ | ||
| parsed_url = urllib.parse.urlparse(url) | ||
| if parsed_url.scheme != "https": | ||
| raise ValueError(f"Endpoint URL ({url}) must use the 'https' scheme. Got '{parsed_url.scheme}' instead.") | ||
| if parsed_url.username or parsed_url.password: | ||
| raise ValueError(f"Endpoint URL ({url}) must not contain username or password.") | ||
| if parsed_url.fragment: | ||
| raise ValueError(f"Endpoint URL ({url}) must not contain a fragment.") | ||
| if parsed_url.query: | ||
| raise ValueError(f"Endpoint URL ({url}) must not contain query parameters.") | ||
|
|
||
| def _load_ca_file_to_data(self) -> None: | ||
| """Load CA file content into ca_data and track modification time. | ||
|
|
||
| :raises ValueError: If the CA file is empty on first read. | ||
| """ | ||
| try: | ||
| with open(self._ca_file, "r", encoding="utf-8") as f: | ||
| content = f.read() | ||
|
pvaneck marked this conversation as resolved.
|
||
|
|
||
| # Check if the file is empty | ||
| if not content: | ||
| # If no prior ca_data exists (first read), fail | ||
| if self._ca_data is None: | ||
| raise ValueError(f"CA file ({self._ca_file}) is empty. Cannot establish secure connection.") | ||
| # If we had prior ca_data, keep it (mid-rotation scenario) | ||
| return | ||
|
|
||
| # File has content, update ca_data and tracking | ||
| self._ca_data = content | ||
| self._ca_file_mtime = os.path.getmtime(self._ca_file) | ||
| except (OSError, IOError) as e: | ||
| # If no prior ca_data exists (first read), fail | ||
| if self._ca_data is None: | ||
| raise ValueError(f"Failed to read CA file ({self._ca_file}): {e}") from e | ||
| # If we can't read the file, keep existing ca_data but clear mtime | ||
| # so we'll try to reload on the next change check | ||
| self._ca_file_mtime = None | ||
|
|
||
| def _has_ca_file_changed(self) -> bool: | ||
| """Check if the CA file has changed since last load. | ||
|
|
||
| :return: True if the CA file has changed, False otherwise. | ||
| :rtype: bool | ||
| """ | ||
| if not self._ca_file: | ||
| return False | ||
|
|
||
| if not os.path.exists(self._ca_file): | ||
| # File was deleted, consider this a change if we had data before | ||
| return self._ca_data is not None or self._ca_file_mtime is not None | ||
|
|
||
| try: | ||
| # Check modification time | ||
| current_mtime = os.path.getmtime(self._ca_file) | ||
| return self._ca_file_mtime != current_mtime | ||
| except (OSError, IOError): | ||
| # If we can't read the file stats, assume it changed | ||
| return True | ||
|
|
||
| def _update_request_url(self, request: HttpRequest) -> None: | ||
| """Update the request URL to use proxy endpoint if configured. | ||
|
|
||
| :param request: The HTTP request object to update. | ||
| :type request: ~azure.core.rest.HttpRequest | ||
| """ | ||
| if self._proxy_endpoint: | ||
| parsed_request_url = urllib.parse.urlparse(request.url) | ||
| parsed_proxy_url = urllib.parse.urlparse(self._proxy_endpoint) | ||
| combined_path = parsed_proxy_url.path.rstrip("/") + "/" + parsed_request_url.path.lstrip("/") | ||
| new_url = urllib.parse.urlunparse( | ||
| ( | ||
| parsed_proxy_url.scheme, | ||
| parsed_proxy_url.netloc, | ||
| combined_path, | ||
|
pvaneck marked this conversation as resolved.
|
||
| parsed_request_url.params, | ||
| parsed_request_url.query, | ||
| parsed_request_url.fragment, | ||
| ) | ||
| ) | ||
| request.url = new_url | ||
61 changes: 61 additions & 0 deletions
61
sdk/identity/azure-identity/azure/identity/_internal/token_binding_transport_requests.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| # ------------------------------------ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
| # ------------------------------------ | ||
| """ | ||
| Requests transport class for WorkloadIdentityCredential with token proxy support. | ||
| """ | ||
| import ssl | ||
| from typing import Any, Optional | ||
|
|
||
| from requests.adapters import HTTPAdapter | ||
| from requests import Session | ||
| from azure.core.pipeline.transport import ( # pylint: disable=non-abstract-transport-import, no-name-in-module | ||
| RequestsTransport, | ||
| ) | ||
| from azure.core.rest import HttpRequest | ||
|
|
||
| from .token_binding_transport_mixin import TokenBindingTransportMixin | ||
|
|
||
|
|
||
| class SNIAdapter(HTTPAdapter): | ||
| """A custom HTTPAdapter that allows setting a custom SNI hostname.""" | ||
|
|
||
| def __init__(self, server_hostname: Optional[str], ca_data: Optional[str], **kwargs: Any) -> None: | ||
| self.server_hostname = server_hostname | ||
| self.ca_data = ca_data | ||
| super().__init__(**kwargs) | ||
|
|
||
| def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **pool_kwargs: Any) -> None: | ||
| if self.server_hostname: | ||
| pool_kwargs["server_hostname"] = self.server_hostname | ||
| pool_kwargs["ssl_context"] = ssl.create_default_context(cadata=self.ca_data) | ||
| super().init_poolmanager(connections, maxsize, block, **pool_kwargs) | ||
|
|
||
|
|
||
| class CustomRequestsTransport(TokenBindingTransportMixin, RequestsTransport): | ||
| """Custom RequestsTransport with SNI and CA certificate support for WorkloadIdentityCredential.""" | ||
|
|
||
| def __init__(self, *args: Any, **kwargs: Any) -> None: | ||
| self.session: Optional[Session] = None | ||
| super().__init__(*args, **kwargs) | ||
| self._update_adaptor() | ||
|
|
||
| def _update_adaptor(self) -> None: | ||
| """Update the session's adapter with the current SNI and CA data.""" | ||
| if not self.session: | ||
| self.session = Session() | ||
|
|
||
| adapter = SNIAdapter(self._sni, self._ca_data) | ||
| self.session.mount("https://", adapter) | ||
|
xiangyan99 marked this conversation as resolved.
|
||
|
|
||
| def send(self, request: HttpRequest, **kwargs: Any) -> Any: | ||
| self._update_request_url(request) | ||
|
|
||
| # Check if CA file has changed and reload ca_data if needed | ||
| if self._ca_file and self._has_ca_file_changed(): | ||
| self._load_ca_file_to_data() | ||
| # If ca_data was updated, recreate SSL context with the new data | ||
| if self._ca_data: | ||
| self._update_adaptor() | ||
| return super().send(request, **kwargs) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.