Skip to content
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

[Core] Drop Track 1 SDK authentication #29631

Merged
merged 1 commit into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 10 additions & 16 deletions src/azure-cli-core/azure/cli/core/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,16 +385,8 @@ def logout_all(self):
identity.logout_all_users()
identity.logout_all_service_principal()

def get_login_credentials(self, resource=None, subscription_id=None, aux_subscriptions=None, aux_tenants=None):
"""Get a CredentialAdaptor instance to be used with both Track 1 and Track 2 SDKs.

:param resource: The resource ID to acquire an access token. Only provide it for Track 1 SDKs.
:param subscription_id:
:param aux_subscriptions:
:param aux_tenants:
"""
resource = resource or self.cli_ctx.cloud.endpoints.active_directory_resource_id

def get_login_credentials(self, subscription_id=None, aux_subscriptions=None, aux_tenants=None):
Copy link
Member Author

Choose a reason for hiding this comment

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

For Track 1 SDK, the resource of the access token is managed by Azure CLI, not Track 1 SDK. resource is kept as a property of the credential returned by get_login_credentials(). When SDK client calls signed_session() to acquire the access token, CLI provides the access token for resource.

                                returns
get_login_credentials(resource) ------>  credential[resource]
                                             ^
                                             |   calls signed_session()
                                             |
                                  Track 1 SDK client()

For Track 2 SDK, the scopes(resource/.default) of the access token is managed by SDK client instead. The scopes is passed to Track 2 SDK via credential_scopes argument when creating the client instance. The SDK client keeps it and passes it to get_token() when acquiring the access token from the credential.

                            returns
get_login_credentials()  ------------->  credential()
                                             ^
                                             |   calls get_token(scopes)
                                             |
                             Track 2 SDK client(credential_scopes)

Since Track 1 SDK is no longer supported, Azure CLI doesn't need to manage resource anymore, so resource argument is dropped.

"""Get a credential compatible with Track 2 SDK."""
if aux_tenants and aux_subscriptions:
raise CLIError("Please specify only one of aux_subscriptions and aux_tenants, not both")

Expand All @@ -407,17 +399,21 @@ def get_login_credentials(self, resource=None, subscription_id=None, aux_subscri
from .auth.msal_credentials import CloudShellCredential
from azure.cli.core.auth.credential_adaptor import CredentialAdaptor
# The credential must be wrapped by CredentialAdaptor so that it can work with Track 1 SDKs.
cred = CredentialAdaptor(CloudShellCredential(), resource=resource)
cred = CredentialAdaptor(CloudShellCredential())

elif managed_identity_type:
# managed identity
if _on_azure_arc():
from .auth.msal_credentials import ManagedIdentityCredential
from azure.cli.core.auth.credential_adaptor import CredentialAdaptor
# The credential must be wrapped by CredentialAdaptor so that it can work with Track 1 SDKs.
cred = CredentialAdaptor(ManagedIdentityCredential(), resource=resource)
cred = CredentialAdaptor(ManagedIdentityCredential())
else:
cred = MsiAccountTypes.msi_auth_factory(managed_identity_type, managed_identity_id, resource)
# The resource is merely used by msrestazure to get the first access token.
# It is not actually used in an API invocation.
cred = MsiAccountTypes.msi_auth_factory(
managed_identity_type, managed_identity_id,
self.cli_ctx.cloud.endpoints.active_directory_resource_id)

else:
# user and service principal
Expand All @@ -436,9 +432,7 @@ def get_login_credentials(self, resource=None, subscription_id=None, aux_subscri
for external_tenant in external_tenants:
external_credentials.append(self._create_credential(account, tenant_id=external_tenant))
from azure.cli.core.auth.credential_adaptor import CredentialAdaptor
cred = CredentialAdaptor(credential,
auxiliary_credentials=external_credentials,
resource=resource)
cred = CredentialAdaptor(credential, auxiliary_credentials=external_credentials)

return (cred,
str(account[_SUBSCRIPTION_ID]),
Expand Down
44 changes: 7 additions & 37 deletions src/azure-cli-core/azure/cli/core/auth/credential_adaptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,39 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import requests
from knack.log import get_logger
from knack.util import CLIError

from .util import resource_to_scopes

logger = get_logger(__name__)


class CredentialAdaptor:
def __init__(self, credential, resource=None, auxiliary_credentials=None):
"""
Adaptor to both
- Track 1: msrest.authentication.Authentication, which exposes signed_session
- Track 2: azure.core.credentials.TokenCredential, which exposes get_token
def __init__(self, credential, auxiliary_credentials=None):
"""Cross-tenant credential adaptor. It takes a main credential and auxiliary credentials.

It implements Track 2 SDK's azure.core.credentials.TokenCredential by exposing get_token.
Comment on lines +12 to +15
Copy link
Member Author

Choose a reason for hiding this comment

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

Even though Track 1 SDK auth support is dropped, CredentialAdaptor is preserved as it will be repurposed as an SDK-MSAL adaptor in #29955.


:param credential: Main credential from .msal_authentication
:param resource: AAD resource for Track 1 only
:param auxiliary_credentials: Credentials from .msal_authentication for cross tenant authentication.
Details about cross tenant authentication:
https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/authenticate-multi-tenant
"""

self._credential = credential
self._auxiliary_credentials = auxiliary_credentials
self._resource = resource

def _get_token(self, scopes=None, **kwargs):
external_tenant_tokens = []
# If scopes is not provided, use CLI-managed resource
scopes = scopes or resource_to_scopes(self._resource)
try:
token = self._credential.get_token(*scopes, **kwargs)
if self._auxiliary_credentials:
external_tenant_tokens = [cred.get_token(*scopes) for cred in self._auxiliary_credentials]
return token, external_tenant_tokens
except requests.exceptions.SSLError as err:
from azure.cli.core.util import SSLERROR_TEMPLATE
raise CLIError(SSLERROR_TEMPLATE.format(str(err)))

def signed_session(self, session=None):
logger.debug("CredentialAdaptor.signed_session")
session = session or requests.Session()
token, external_tenant_tokens = self._get_token()
header = "{} {}".format('Bearer', token.token)
session.headers['Authorization'] = header
if external_tenant_tokens:
aux_tokens = ';'.join(['{} {}'.format('Bearer', tokens2.token) for tokens2 in external_tenant_tokens])
session.headers['x-ms-authorization-auxiliary'] = aux_tokens
return session

def get_token(self, *scopes, **kwargs):
"""Get an access token from the main credential."""
logger.debug("CredentialAdaptor.get_token: scopes=%r, kwargs=%r", scopes, kwargs)

# Discard unsupported kwargs: tenant_id, enable_cae
filtered_kwargs = {}
if 'data' in kwargs:
filtered_kwargs['data'] = kwargs['data']

token, _ = self._get_token(scopes, **filtered_kwargs)
return token
return self._credential.get_token(*scopes, **filtered_kwargs)
Copy link
Member

Choose a reason for hiding this comment

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

I noticed that SSLError try catch part is removed. Any reason?

Copy link
Member Author

Choose a reason for hiding this comment

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

SSLError should be handled by azure.cli.core.util.handle_exception:

# SSLError is raised when making HTTP requests with 'requests' lib behind a proxy that intercepts HTTPS traffic.
# - SSLError is raised when directly calling 'requests' lib, such as MSAL or `az rest`
# - azure.core.exceptions.ServiceRequestError is raised when indirectly calling 'requests' lib with azure.core,
# which wraps the original SSLError
elif isinstance(ex, SSLError) or isinstance(ex, ServiceRequestError) and isinstance(ex.inner_exception, SSLError):
az_error = azclierror.AzureConnectionError(error_msg)
az_error.set_recommendation(SSLERROR_TEMPLATE)

SSLError handling comes from ADAL age: #11093. I can't remember why I added it to src/azure-cli-core/azure/cli/core/adal_authentication.py instead of src/azure-cli-core/azure/cli/core/util.py.


def get_auxiliary_tokens(self, *scopes, **kwargs):
"""Get access tokens from auxiliary credentials."""
# To test cross-tenant authentication, see https://github.com/Azure/azure-cli/issues/16691
if self._auxiliary_credentials:
return [cred.get_token(*scopes, **kwargs) for cred in self._auxiliary_credentials]
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ def _get_mgmt_service_client(cli_ctx,
subscription_id=None,
api_version=None,
base_url_bound=True,
resource=None,
sdk_profile=None,
aux_subscriptions=None,
aux_tenants=None,
Expand All @@ -222,10 +221,6 @@ def _get_mgmt_service_client(cli_ctx,
from azure.cli.core._profile import Profile
logger.debug('Getting management service client client_type=%s', client_type.__name__)

# Track 1 SDK doesn't maintain the `resource`. The `resource` of the token is the one passed to
# get_login_credentials.
resource = resource or cli_ctx.cloud.endpoints.active_directory_resource_id

if credential:
# Use a custom credential
if not subscription_id:
Expand All @@ -234,7 +229,7 @@ def _get_mgmt_service_client(cli_ctx,
# Get a credential for the current `az login` context
profile = Profile(cli_ctx=cli_ctx)
credential, subscription_id, _ = profile.get_login_credentials(
subscription_id=subscription_id, resource=resource,
subscription_id=subscription_id,
aux_subscriptions=aux_subscriptions, aux_tenants=aux_tenants)

client_kwargs = {}
Expand Down