-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Preview implementation of AzureCliCredential #10092
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 11 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
6228b6d
PoC CliCredentials
lmazuel 8e5e2bb
Fix MSAL scope poststuff
lmazuel 38be4ef
Cli credentials (#9162)
cxznmhdcxz 8cd95d0
revise credential
chlowell 990761a
revise tests
chlowell 3dc868b
async credential wraps the sync one
chlowell 1abfba1
revise async tests
chlowell a24c282
add async credential to default
chlowell 0b975bb
tests for default credential exclusion option
chlowell 8a5f0a7
update changelog
chlowell 765c01e
update version
chlowell 07fa8c4
asyncio implementation
chlowell 0bd2916
fix platform restrictions on test cases
chlowell 5163bdd
fall back to known install locations when PATH isn't set
chlowell 5a56278
AzureCliCredential is internal
chlowell 17e090c
update changelog
chlowell 6b678cf
this is a beta package
chlowell 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
129 changes: 129 additions & 0 deletions
129
sdk/identity/azure-identity/azure/identity/_credentials/azure_cli.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,129 @@ | ||
| # ------------------------------------ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
| # ------------------------------------ | ||
| from datetime import datetime | ||
| import json | ||
| import os | ||
| import platform | ||
| import re | ||
| import sys | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| import subprocess | ||
|
|
||
| from azure.core.credentials import AccessToken | ||
| from azure.core.exceptions import ClientAuthenticationError | ||
|
|
||
| from .. import CredentialUnavailableError | ||
| from .._internal import _scopes_to_resource | ||
|
|
||
| if TYPE_CHECKING: | ||
| # pylint:disable=ungrouped-imports | ||
| from typing import Any | ||
|
|
||
| CLI_NOT_FOUND = "Azure CLI not found on path" | ||
| COMMAND_LINE = "az account get-access-token --output json --resource {}" | ||
|
|
||
| # CLI's "expiresOn" is naive, so we use this naive datetime for the epoch to calculate expires_on in UTC | ||
| EPOCH = datetime.fromtimestamp(0) | ||
|
|
||
|
|
||
| class AzureCliCredential(object): | ||
| """Authenticates by requesting a token from the Azure CLI. | ||
|
|
||
| This requires previously logging in to Azure via "az login", and will use the CLI's currently logged in identity. | ||
| """ | ||
|
|
||
| def get_token(self, *scopes, **kwargs): # pylint:disable=no-self-use,unused-argument | ||
| # type: (*str, **Any) -> AccessToken | ||
| """Request an access token for `scopes`. | ||
|
|
||
| .. note:: This method is called by Azure SDK clients. It isn't intended for use in application code. | ||
|
|
||
| Only one scope is supported per request. This credential won't cache tokens. Every call invokes the Azure CLI. | ||
|
|
||
| :param str scopes: desired scopes for the token. Only **one** scope is supported per call. | ||
| :rtype: :class:`azure.core.credentials.AccessToken` | ||
|
|
||
| :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI. | ||
| :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't | ||
| receive an access token. | ||
| """ | ||
|
|
||
| resource = _scopes_to_resource(*scopes) | ||
| output, error = _run_command(COMMAND_LINE.format(resource)) | ||
| if error: | ||
| raise error | ||
|
|
||
| token = parse_token(output) | ||
| if not token: | ||
| sanitized_output = sanitize_output(output) | ||
| raise ClientAuthenticationError(message="Unexpected output from Azure CLI: '{}'".format(sanitized_output)) | ||
|
|
||
| return token | ||
|
|
||
|
|
||
| def parse_token(output): | ||
| """Parse output of 'az account get-access-token' to an AccessToken. | ||
|
|
||
| In particular, convert the CLI's "expiresOn" value, the string representation of a naive datetime, to epoch seconds. | ||
| """ | ||
| try: | ||
| token = json.loads(output) | ||
| parsed_expires_on = datetime.strptime(token["expiresOn"], "%Y-%m-%d %H:%M:%S.%f") | ||
|
|
||
| # calculate seconds since the epoch; parsed_expires_on and EPOCH are naive | ||
| expires_on = (parsed_expires_on - EPOCH).total_seconds() | ||
|
|
||
| return AccessToken(token["accessToken"], int(expires_on)) | ||
| except (KeyError, ValueError): | ||
| return None | ||
|
|
||
|
|
||
| def get_safe_working_dir(): | ||
| """Invoke 'az' from a directory on $PATH to get 'az' from the path, not the executing program's directory""" | ||
|
|
||
| path = os.environ["PATH"] | ||
| if sys.platform.startswith("win"): | ||
| return path.split(";")[0] | ||
| return path.split(":")[0] | ||
|
|
||
|
|
||
| def sanitize_output(output): | ||
| """Redact access tokens from CLI output to prevent error messages revealing them""" | ||
| return re.sub(r"\"accessToken\": \"(.*?)(\"|$)", "****", output) | ||
|
|
||
|
|
||
| def _run_command(command): | ||
| if sys.platform.startswith("win"): | ||
| args = ["cmd", "/c", command] | ||
| else: | ||
| args = ["/bin/sh", "-c", command] | ||
| try: | ||
| working_directory = get_safe_working_dir() | ||
|
|
||
| kwargs = {"stderr": subprocess.STDOUT, "cwd": working_directory, "universal_newlines": True} | ||
| if platform.python_version() >= "3.3": | ||
| kwargs["timeout"] = 10 | ||
|
|
||
| output = subprocess.check_output(args, **kwargs) | ||
| return output, None | ||
| except subprocess.CalledProcessError as ex: | ||
| # non-zero return from shell | ||
| if ex.returncode == 127 or ex.output.startswith("'az' is not recognized"): | ||
| error = CredentialUnavailableError(message=CLI_NOT_FOUND) | ||
| else: | ||
| # return code is from the CLI -> propagate its output | ||
| if ex.output: | ||
| message = sanitize_output(ex.output) | ||
| else: | ||
| message = "Failed to invoke Azure CLI" | ||
| error = ClientAuthenticationError(message=message) | ||
| except OSError as ex: | ||
| # failed to execute 'cmd' or '/bin/sh'; CLI may or may not be installed | ||
| error = CredentialUnavailableError(message="Failed to execute '{}'".format(args[0])) | ||
| except Exception as ex: # pylint:disable=broad-except | ||
| error = ex | ||
|
|
||
| return None, error |
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
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
32 changes: 32 additions & 0 deletions
32
sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_cli.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,32 @@ | ||
| # ------------------------------------ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
| # ------------------------------------ | ||
| from .._credentials.base import AsyncCredentialBase | ||
| from ..._credentials import AzureCliCredential as _SyncAzureCliCredential | ||
|
|
||
|
|
||
| class AzureCliCredential(AsyncCredentialBase): | ||
| """Authenticates by requesting a token from the Azure CLI. | ||
|
|
||
| This requires previously logging in to Azure via "az login", and will use the CLI's currently logged in identity. | ||
| """ | ||
|
|
||
| async def get_token(self, *scopes, **kwargs): | ||
| """Request an access token for `scopes`. | ||
|
|
||
| .. note:: This method is called by Azure SDK clients. It isn't intended for use in application code. | ||
|
|
||
| Only one scope is supported per request. This credential won't cache tokens. Every call invokes the Azure CLI. | ||
|
|
||
| :param str scopes: desired scopes for the token. Only **one** scope is supported per call. | ||
| :rtype: :class:`azure.core.credentials.AccessToken` | ||
|
|
||
| :raises ~azure.identity.CredentialUnavailableError: the credential was unable to invoke the Azure CLI. | ||
| :raises ~azure.core.exceptions.ClientAuthenticationError: the credential invoked the Azure CLI but didn't | ||
| receive an access token. | ||
| """ | ||
| return _SyncAzureCliCredential().get_token(*scopes, **kwargs) | ||
|
|
||
| async def close(self): | ||
| return |
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.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please sync with rest of lang devs to ensure we are on the same page across langs on whether or not DAC has CLICred by default.
I highly prefer it to be enabled by default so developer doesn't have to do anything to configure it.