Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package azure-cli-core for openSUSE:Factory checked in at 2025-04-02 17:14:30 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/azure-cli-core (Old) and /work/SRC/openSUSE:Factory/.azure-cli-core.new.1907 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "azure-cli-core" Wed Apr 2 17:14:30 2025 rev:79 rq:1266203 version:2.71.0 Changes: -------- --- /work/SRC/openSUSE:Factory/azure-cli-core/azure-cli-core.changes 2025-03-13 15:08:32.936117957 +0100 +++ /work/SRC/openSUSE:Factory/.azure-cli-core.new.1907/azure-cli-core.changes 2025-04-02 17:16:08.136327261 +0200 @@ -1,0 +2,9 @@ +Tue Apr 1 06:16:56 UTC 2025 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- New upstream release + + Version 2.71.0 + + For detailed information about changes see the + HISTORY.rst file provided with this package +- Update Requires from setup.py + +------------------------------------------------------------------- Old: ---- azure_cli_core-2.70.0.tar.gz New: ---- azure_cli_core-2.71.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ azure-cli-core.spec ++++++ --- /var/tmp/diff_new_pack.yxKNtX/_old 2025-04-02 17:16:09.708393224 +0200 +++ /var/tmp/diff_new_pack.yxKNtX/_new 2025-04-02 17:16:09.708393224 +0200 @@ -24,7 +24,7 @@ %global _sitelibdir %{%{pythons}_sitelib} Name: azure-cli-core -Version: 2.70.0 +Version: 2.71.0 Release: 0 Summary: Microsoft Azure CLI Core Module License: MIT @@ -50,7 +50,7 @@ Requires: %{pythons}-jmespath Requires: %{pythons}-knack < 1.0.0 Requires: %{pythons}-knack >= 0.11.0 -Requires: %{pythons}-microsoft-security-utilities-secret-masker >= 1.0.0~b2 +Requires: %{pythons}-microsoft-security-utilities-secret-masker >= 1.0.0~b4 Requires: %{pythons}-msal < 2.0.0 Requires: %{pythons}-msal >= 1.31.2~b1 Requires: %{pythons}-msal-extensions < 2.0.0 ++++++ azure_cli_core-2.70.0.tar.gz -> azure_cli_core-2.71.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/HISTORY.rst new/azure_cli_core-2.71.0/HISTORY.rst --- old/azure_cli_core-2.70.0/HISTORY.rst 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/HISTORY.rst 2025-03-25 10:17:55.000000000 +0100 @@ -3,6 +3,10 @@ Release History =============== +2.71.0 +++++++ +* PREVIEW: Support managed identity authentication with MSAL. Run `az config set core.use_msal_managed_identity=true` or set environment variable `AZURE_CORE_USE_MSAL_MANAGED_IDENTITY` to enable it (#31092) + 2.70.0 ++++++ * Resolve CVE-2024-12797 (#30816) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/PKG-INFO new/azure_cli_core-2.71.0/PKG-INFO --- old/azure_cli_core-2.70.0/PKG-INFO 2025-02-26 07:24:39.789584600 +0100 +++ new/azure_cli_core-2.71.0/PKG-INFO 2025-03-25 10:18:48.041750200 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 2.2 +Metadata-Version: 2.4 Name: azure-cli-core -Version: 2.70.0 +Version: 2.71.0 Summary: Microsoft Azure Command-Line Tools Core Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation @@ -26,7 +26,7 @@ Requires-Dist: humanfriendly~=10.0 Requires-Dist: jmespath Requires-Dist: knack~=0.11.0 -Requires-Dist: microsoft-security-utilities-secret-masker~=1.0.0b2 +Requires-Dist: microsoft-security-utilities-secret-masker~=1.0.0b4 Requires-Dist: msal-extensions==1.2.0 Requires-Dist: msal[broker]==1.31.2b1 Requires-Dist: msrestazure~=0.6.4 @@ -43,6 +43,7 @@ Dynamic: description Dynamic: home-page Dynamic: license +Dynamic: license-file Dynamic: requires-dist Dynamic: requires-python Dynamic: summary diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/__init__.py new/azure_cli_core-2.71.0/azure/cli/core/__init__.py --- old/azure_cli_core-2.70.0/azure/cli/core/__init__.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/__init__.py 2025-03-25 10:17:55.000000000 +0100 @@ -4,7 +4,7 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=line-too-long -__version__ = "2.70.0" +__version__ = "2.71.0" import os import sys @@ -489,6 +489,26 @@ return self.command_table + @staticmethod + def _sort_command_loaders(command_loaders): + module_command_loaders = [] + extension_command_loaders = [] + + # Separate module and extension command loaders + for loader in command_loaders: + if loader.__module__.startswith('azext'): + extension_command_loaders.append(loader) + else: + module_command_loaders.append(loader) + + # Sort name in each command loader list + module_command_loaders.sort(key=lambda loader: loader.__class__.__name__) + extension_command_loaders.sort(key=lambda loader: loader.__class__.__name__) + + # Module first, then extension + sorted_command_loaders = module_command_loaders + extension_command_loaders + return sorted_command_loaders + def load_arguments(self, command=None): from azure.cli.core.commands.parameters import ( resource_group_name_type, get_location_type, deployment_name_type, vnet_name_type, subnet_name_type) @@ -499,6 +519,8 @@ command_loaders = set() for loaders in self.cmd_to_loader_map.values(): command_loaders = command_loaders.union(set(loaders)) + # sort command loaders for consistent order when loading all commands for docs generation to avoid random diff + command_loaders = self._sort_command_loaders(command_loaders) logger.info('Applying %s command loaders...', len(command_loaders)) else: command_loaders = self.cmd_to_loader_map.get(command, None) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/_profile.py new/azure_cli_core-2.71.0/azure/cli/core/_profile.py --- old/azure_cli_core-2.70.0/azure/cli/core/_profile.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/_profile.py 2025-03-25 10:17:55.000000000 +0100 @@ -11,6 +11,7 @@ from azure.cli.core._session import ACCOUNT from azure.cli.core.azclierror import AuthenticationError from azure.cli.core.cloud import get_active_cloud, set_cloud_subscription +from azure.cli.core.auth.credential_adaptor import CredentialAdaptor from azure.cli.core.util import in_cloud_console, can_launch_browser, is_github_codespaces from knack.log import get_logger from knack.util import CLIError @@ -60,11 +61,6 @@ _AZ_LOGIN_MESSAGE = "Please run 'az login' to setup account." -MANAGED_IDENTITY_ID_WARNING = ( - "Passing the managed identity ID with --username is deprecated and will be removed in a future release. " - "Please use --client-id, --object-id or --resource-id instead." -) - def load_subscriptions(cli_ctx, all_clouds=False, refresh=False): profile = Profile(cli_ctx=cli_ctx) @@ -226,9 +222,13 @@ def login_with_managed_identity(self, identity_id=None, client_id=None, object_id=None, resource_id=None, allow_no_subscriptions=None): - if _on_azure_arc(): - return self.login_with_managed_identity_azure_arc( - identity_id=identity_id, allow_no_subscriptions=allow_no_subscriptions) + if _use_msal_managed_identity(self.cli_ctx): + if identity_id: + raise CLIError('--username is not supported by MSAL managed identity. ' + 'Use --client-id, --object-id or --resource-id instead.') + return self.login_with_managed_identity_msal( + client_id=client_id, object_id=object_id, resource_id=resource_id, + allow_no_subscriptions=allow_no_subscriptions) import jwt from azure.mgmt.core.tools import is_valid_resource_id @@ -256,7 +256,6 @@ msi_creds = MSIAuthenticationWrapper(resource=resource, msi_res_id=resource_id) # The old way of re-using the same --username for 3 types of ID elif identity_id: - logger.warning(MANAGED_IDENTITY_ID_WARNING) if is_valid_resource_id(identity_id): msi_creds = MSIAuthenticationWrapper(resource=resource, msi_res_id=identity_id) identity_type = MsiAccountTypes.user_assigned_resource_id @@ -309,21 +308,23 @@ self._set_subscriptions(consolidated) return deepcopy(consolidated) - def login_with_managed_identity_azure_arc(self, identity_id=None, allow_no_subscriptions=None): + def login_with_managed_identity_msal(self, client_id=None, object_id=None, resource_id=None, + allow_no_subscriptions=None): import jwt - identity_type = MsiAccountTypes.system_assigned - from .auth.msal_credentials import ManagedIdentityCredential + from .auth.constants import ACCESS_TOKEN - cred = ManagedIdentityCredential() - token = cred.get_token(*self._arm_scope).token + identity_id_type, identity_id_value = MsiAccountTypes.parse_ids( + client_id=client_id, object_id=object_id, resource_id=resource_id) + cred = MsiAccountTypes.msal_credential_factory(identity_id_type, identity_id_value) + token = cred.acquire_token(self._arm_scope)[ACCESS_TOKEN] logger.info('Managed identity: token was retrieved. Now trying to initialize local accounts...') decode = jwt.decode(token, algorithms=['RS256'], options={"verify_signature": False}) tenant = decode['tid'] subscription_finder = SubscriptionFinder(self.cli_ctx) subscriptions = subscription_finder.find_using_specific_tenant(tenant, cred) - base_name = ('{}-{}'.format(identity_type, identity_id) if identity_id else identity_type) - user = _USER_ASSIGNED_IDENTITY if identity_id else _SYSTEM_ASSIGNED_IDENTITY + base_name = ('{}-{}'.format(identity_id_type, identity_id_value) if identity_id_value else identity_id_type) + user = _USER_ASSIGNED_IDENTITY if identity_id_value else _SYSTEM_ASSIGNED_IDENTITY if not subscriptions: if allow_no_subscriptions: subscriptions = self._build_tenant_level_accounts([tenant]) @@ -339,9 +340,10 @@ def login_in_cloud_shell(self): import jwt from .auth.msal_credentials import CloudShellCredential + from .auth.constants import ACCESS_TOKEN cred = CloudShellCredential() - token = cred.get_token(*self._arm_scope).token + token = cred.acquire_token(self._arm_scope)[ACCESS_TOKEN] logger.info('Cloud Shell token was retrieved. Now trying to initialize local accounts...') decode = jwt.decode(token, algorithms=['RS256'], options={"verify_signature": False}) tenant = decode['tid'] @@ -397,21 +399,19 @@ if in_cloud_console() and account[_USER_ENTITY].get(_CLOUD_SHELL_ID): # Cloud Shell 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()) + sdk_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 + if _use_msal_managed_identity(self.cli_ctx): # The credential must be wrapped by CredentialAdaptor so that it can work with Track 1 SDKs. - cred = CredentialAdaptor(ManagedIdentityCredential()) + cred = MsiAccountTypes.msal_credential_factory(managed_identity_type, managed_identity_id) + sdk_cred = CredentialAdaptor(cred) else: # 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( + sdk_cred = MsiAccountTypes.msi_auth_factory( managed_identity_type, managed_identity_id, self.cli_ctx.cloud.endpoints.active_directory_resource_id) @@ -431,14 +431,14 @@ external_credentials = [] 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) + sdk_cred = CredentialAdaptor(credential, auxiliary_credentials=external_credentials) - return (cred, + return (sdk_cred, str(account[_SUBSCRIPTION_ID]), str(account[_TENANT_ID])) - def get_raw_token(self, resource=None, scopes=None, subscription=None, tenant=None): + def get_raw_token(self, resource=None, scopes=None, subscription=None, tenant=None, credential_out=None): + # credential_out is only used by unit tests to inspect the credential. Do not use it! # Convert resource to scopes if resource and not scopes: from .auth.util import resource_to_scopes @@ -460,24 +460,26 @@ if tenant: raise CLIError("Tenant shouldn't be specified for Cloud Shell account") from .auth.msal_credentials import CloudShellCredential - cred = CloudShellCredential() + sdk_cred = CredentialAdaptor(CloudShellCredential()) elif managed_identity_type: # managed identity if tenant: raise CLIError("Tenant shouldn't be specified for managed identity account") - if _on_azure_arc(): - from .auth.msal_credentials import ManagedIdentityCredential - cred = ManagedIdentityCredential() + if _use_msal_managed_identity(self.cli_ctx): + cred = MsiAccountTypes.msal_credential_factory(managed_identity_type, managed_identity_id) + if credential_out: + credential_out['credential'] = cred + sdk_cred = CredentialAdaptor(cred) else: from .auth.util import scopes_to_resource - cred = MsiAccountTypes.msi_auth_factory(managed_identity_type, managed_identity_id, - scopes_to_resource(scopes)) + sdk_cred = MsiAccountTypes.msi_auth_factory(managed_identity_type, managed_identity_id, + scopes_to_resource(scopes)) else: - cred = self._create_credential(account, tenant_id=tenant) + sdk_cred = CredentialAdaptor(self._create_credential(account, tenant_id=tenant)) - sdk_token = cred.get_token(*scopes) + sdk_token = sdk_cred.get_token(*scopes) # Convert epoch int 'expires_on' to datetime string 'expiresOn' for backward compatibility # WARNING: expiresOn is deprecated and will be removed in future release. import datetime @@ -816,6 +818,41 @@ return MSIAuthenticationWrapper(resource=resource, msi_res_id=identity) raise ValueError("unrecognized msi account name '{}'".format(cli_account_name)) + @staticmethod + def parse_ids(client_id=None, object_id=None, resource_id=None): + id_arg_count = len([arg for arg in (client_id, object_id, resource_id) if arg]) + if id_arg_count > 1: + raise CLIError('Usage error: Provide only one of --client-id, --object-id, --resource-id.') + + id_type = None + id_value = None + if id_arg_count == 0: + id_type = MsiAccountTypes.system_assigned + id_value = None + elif client_id: + id_type = MsiAccountTypes.user_assigned_client_id + id_value = client_id + elif object_id: + id_type = MsiAccountTypes.user_assigned_object_id + id_value = object_id + elif resource_id: + id_type = MsiAccountTypes.user_assigned_resource_id + id_value = resource_id + return id_type, id_value + + @staticmethod + def msal_credential_factory(id_type, id_value): + from azure.cli.core.auth.msal_credentials import ManagedIdentityCredential + if id_type == MsiAccountTypes.system_assigned: + return ManagedIdentityCredential() + if id_type == MsiAccountTypes.user_assigned_client_id: + return ManagedIdentityCredential(client_id=id_value) + if id_type == MsiAccountTypes.user_assigned_object_id: + return ManagedIdentityCredential(object_id=id_value) + if id_type == MsiAccountTypes.user_assigned_resource_id: + return ManagedIdentityCredential(resource_id=id_value) + raise ValueError("Unrecognized managed identity ID type '{}'".format(id_type)) + class SubscriptionFinder: # An ARM client. It finds subscriptions for a user or service principal. It shouldn't do any @@ -856,7 +893,6 @@ specific_tenant_credential = identity.get_user_credential(username) try: - subscriptions = self.find_using_specific_tenant(tenant_id, specific_tenant_credential, tenant_id_description=t) except AuthenticationError as ex: @@ -927,9 +963,12 @@ raise CLIInternalError("Unable to get '{}' in profile '{}'" .format(ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS, self.cli_ctx.cloud.profile)) api_version = get_api_version(self.cli_ctx, ResourceType.MGMT_RESOURCE_SUBSCRIPTIONS) - client_kwargs = _prepare_mgmt_client_kwargs_track2(self.cli_ctx, credential) - client = client_type(credential, api_version=api_version, + # MSIAuthenticationWrapper already implements get_token, so no need to wrap it with CredentialAdaptor + from azure.cli.core.auth.adal_authentication import MSIAuthenticationWrapper + sdk_cred = credential if isinstance(credential, MSIAuthenticationWrapper) else CredentialAdaptor(credential) + client_kwargs = _prepare_mgmt_client_kwargs_track2(self.cli_ctx, sdk_cred) + client = client_type(sdk_cred, api_version=api_version, base_url=self.cli_ctx.cloud.endpoints.resource_manager, **client_kwargs) return client @@ -980,7 +1019,9 @@ instance_discovery=instance_discovery) -def _on_azure_arc(): +def _use_msal_managed_identity(cli_ctx): # This indicates an Azure Arc-enabled server from msal.managed_identity import get_managed_identity_source, AZURE_ARC - return get_managed_identity_source() == AZURE_ARC + # PREVIEW: Use core.use_msal_managed_identity=true to enable managed identity authentication with MSAL + use_msal_managed_identity = cli_ctx.config.getboolean('core', 'use_msal_managed_identity', fallback=False) + return use_msal_managed_identity or get_managed_identity_source() == AZURE_ARC diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/auth/constants.py new/azure_cli_core-2.71.0/azure/cli/core/auth/constants.py --- old/azure_cli_core-2.70.0/azure/cli/core/auth/constants.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/auth/constants.py 2025-03-25 10:17:55.000000000 +0100 @@ -4,3 +4,6 @@ # -------------------------------------------------------------------------------------------- AZURE_CLI_CLIENT_ID = '04b07795-8ddb-461a-bbee-02f9e1bf7b46' + +ACCESS_TOKEN = 'access_token' +EXPIRES_IN = "expires_in" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/auth/credential_adaptor.py new/azure_cli_core-2.71.0/azure/cli/core/auth/credential_adaptor.py --- old/azure_cli_core-2.70.0/azure/cli/core/auth/credential_adaptor.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/auth/credential_adaptor.py 2025-03-25 10:17:55.000000000 +0100 @@ -4,19 +4,19 @@ # -------------------------------------------------------------------------------------------- from knack.log import get_logger +from .util import build_sdk_access_token logger = get_logger(__name__) class CredentialAdaptor: def __init__(self, credential, auxiliary_credentials=None): - """Cross-tenant credential adaptor. It takes a main credential and auxiliary credentials. - + """Credential adaptor between MSAL credential and SDK credential. It implements Track 2 SDK's azure.core.credentials.TokenCredential by exposing get_token. - :param credential: Main credential from .msal_authentication - :param auxiliary_credentials: Credentials from .msal_authentication for cross tenant authentication. - Details about cross tenant authentication: + :param credential: MSAL credential from ._msal_credentials + :param auxiliary_credentials: MSAL credentials for cross-tenant authentication. + Details about cross-tenant authentication: https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/authenticate-multi-tenant """ @@ -24,19 +24,59 @@ self._auxiliary_credentials = auxiliary_credentials def get_token(self, *scopes, **kwargs): - """Get an access token from the main credential.""" + """Implement the old SDK token protocol azure.core.credentials.TokenCredential + Return azure.core.credentials.AccessToken + """ 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'] + msal_kwargs = _prepare_msal_kwargs(kwargs) + msal_result = self._credential.acquire_token(list(scopes), **msal_kwargs) + return build_sdk_access_token(msal_result) + + def get_token_info(self, *scopes, options=None): + """Implement the new SDK token protocol azure.core.credentials.SupportsTokenInfo + Return azure.core.credentials.AccessTokenInfo + """ + logger.debug("CredentialAdaptor.get_token_info: scopes=%r, options=%r", scopes, options) - return self._credential.get_token(*scopes, **filtered_kwargs) + msal_kwargs = _prepare_msal_kwargs(options) + msal_result = self._credential.acquire_token(list(scopes), **msal_kwargs) + return _build_sdk_access_token_info(msal_result) 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] + return [build_sdk_access_token(cred.acquire_token(list(scopes), **kwargs)) + for cred in self._auxiliary_credentials] return None + + +def _prepare_msal_kwargs(options=None): + # Preserve supported options and discard unsupported options (tenant_id, enable_cae). + # Both get_token's kwargs and get_token_info's options are accepted as their schema is the same (at least for now). + msal_kwargs = {} + if options: + # For VM SSH. 'data' support is a CLI-specific extension. + # SDK doesn't support 'data': https://github.com/Azure/azure-sdk-for-python/pull/16397 + if 'data' in options: + msal_kwargs['data'] = options['data'] + # For CAE + if 'claims' in options: + msal_kwargs['claims_challenge'] = options['claims'] + return msal_kwargs + + +def _build_sdk_access_token_info(token_entry): + # MSAL token entry sample: + # { + # 'access_token': 'eyJ0eXAiOiJKV...', + # 'token_type': 'Bearer', + # 'expires_in': 1618, + # 'token_source': 'cache' + # } + from .constants import ACCESS_TOKEN, EXPIRES_IN + from .util import _now_timestamp + from azure.core.credentials import AccessTokenInfo + + return AccessTokenInfo(token_entry[ACCESS_TOKEN], _now_timestamp() + token_entry[EXPIRES_IN]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/auth/identity.py new/azure_cli_core-2.71.0/azure/cli/core/auth/identity.py --- old/azure_cli_core-2.70.0/azure/cli/core/auth/identity.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/auth/identity.py 2025-03-25 10:17:55.000000000 +0100 @@ -41,10 +41,7 @@ class Identity: # pylint: disable=too-many-instance-attributes - """Class to manage identities: - - user - - service principal - - TODO: managed identity + """Manage user or service principal identities and log into Microsoft identity platform. """ # MSAL token cache. @@ -192,20 +189,13 @@ """ sp_auth = ServicePrincipalAuth.build_from_credential(self.tenant_id, client_id, credential) client_credential = sp_auth.get_msal_client_credential() - cca = ConfidentialClientApplication(client_id, client_credential=client_credential, **self._msal_app_kwargs) - result = cca.acquire_token_for_client(scopes) - check_result(result) + cred = ServicePrincipalCredential(client_id, client_credential, **self._msal_app_kwargs) + cred.acquire_token(scopes) # Only persist the service principal after a successful login entry = sp_auth.get_entry_to_persist() self._service_principal_store.save_entry(entry) - def login_with_managed_identity(self, scopes, identity_id=None): # pylint: disable=too-many-statements - raise NotImplementedError - - def login_in_cloud_shell(self, scopes): - raise NotImplementedError - def logout_user(self, username): # If username is an SP client ID, it is ignored accounts = self._msal_app.get_accounts(username) @@ -252,9 +242,6 @@ client_credential = ServicePrincipalAuth(entry).get_msal_client_credential() return ServicePrincipalCredential(client_id, client_credential, **self._msal_app_kwargs) - def get_managed_identity_credential(self, client_id=None): - raise NotImplementedError - class ServicePrincipalAuth: # pylint: disable=too-many-instance-attributes def __init__(self, entry): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/auth/msal_credentials.py new/azure_cli_core-2.71.0/azure/cli/core/auth/msal_credentials.py --- old/azure_cli_core-2.70.0/azure/cli/core/auth/msal_credentials.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/auth/msal_credentials.py 2025-03-25 10:17:55.000000000 +0100 @@ -4,25 +4,16 @@ # -------------------------------------------------------------------------------------------- """ -Credentials defined in this module are alternative implementations of credentials provided by Azure Identity. - -These credentials implement azure.core.credentials.TokenCredential by exposing `get_token` method for Track 2 -SDK invocation. - -If you want to implement your own credential, the credential must also expose `get_token` method. - -`get_token` method takes `scopes` as positional arguments and other optional `kwargs`, such as `claims`, `data`. -The return value should be a named tuple containing two elements: token (str), expires_on (int). You may simply use -azure.cli.core.auth.util.AccessToken to build the return value. See below credentials as examples. +Credentials to acquire tokens from MSAL. """ from knack.log import get_logger from knack.util import CLIError from msal import (PublicClientApplication, ConfidentialClientApplication, - ManagedIdentityClient, SystemAssignedManagedIdentity) + ManagedIdentityClient, SystemAssignedManagedIdentity, UserAssignedManagedIdentity) from .constants import AZURE_CLI_CLIENT_ID -from .util import check_result, build_sdk_access_token +from .util import check_result logger = get_logger(__name__) @@ -30,7 +21,7 @@ class UserCredential: # pylint: disable=too-few-public-methods def __init__(self, client_id, username, **kwargs): - """User credential implementing get_token interface. + """User credential wrapping msal.application.PublicClientApplication :param client_id: Client ID of the CLI. :param username: The username for user credential. @@ -52,20 +43,23 @@ self._account = accounts[0] - def get_token(self, *scopes, claims=None, **kwargs): - # scopes = ['https://pas.windows.net/CheckMyAccess/Linux/.default'] - logger.debug("UserCredential.get_token: scopes=%r, claims=%r, kwargs=%r", scopes, claims, kwargs) + def acquire_token(self, scopes, claims_challenge=None, **kwargs): + # scopes must be a list. + # For acquiring SSH certificate, scopes is ['https://pas.windows.net/CheckMyAccess/Linux/.default'] + # kwargs is already sanitized by CredentialAdaptor, so it can be safely passed to MSAL + logger.debug("UserCredential.acquire_token: scopes=%r, claims_challenge=%r, kwargs=%r", + scopes, claims_challenge, kwargs) - if claims: + if claims_challenge: logger.warning('Acquiring new access token silently for tenant %s with claims challenge: %s', - self._msal_app.authority.tenant, claims) - result = self._msal_app.acquire_token_silent_with_error(list(scopes), self._account, claims_challenge=claims, - **kwargs) + self._msal_app.authority.tenant, claims_challenge) + result = self._msal_app.acquire_token_silent_with_error( + scopes, self._account, claims_challenge=claims_challenge, **kwargs) from azure.cli.core.azclierror import AuthenticationError try: # Check if an access token is returned. - check_result(result, scopes=scopes, claims=claims) + check_result(result, scopes=scopes, claims_challenge=claims_challenge) except AuthenticationError as ex: # For VM SSH ('data' is passed), if getting access token fails because # Conditional Access MFA step-up or compliance check is required, re-launch @@ -82,7 +76,7 @@ success_template, error_template = read_response_templates() result = self._msal_app.acquire_token_interactive( - list(scopes), login_hint=self._account['username'], + scopes, login_hint=self._account['username'], port=8400 if self._msal_app.authority.is_adfs else None, success_template=success_template, error_template=error_template, **kwargs) check_result(result) @@ -91,25 +85,24 @@ # launch browser, but show the error message and `az login` command instead. else: raise - return build_sdk_access_token(result) + return result class ServicePrincipalCredential: # pylint: disable=too-few-public-methods def __init__(self, client_id, client_credential, **kwargs): - """Service principal credential implementing get_token interface. + """Service principal credential wrapping msal.application.ConfidentialClientApplication. :param client_id: The service principal's client ID. :param client_credential: client_credential that will be passed to MSAL. """ - self._msal_app = ConfidentialClientApplication(client_id, client_credential, **kwargs) + self._msal_app = ConfidentialClientApplication(client_id, client_credential=client_credential, **kwargs) - def get_token(self, *scopes, **kwargs): - logger.debug("ServicePrincipalCredential.get_token: scopes=%r, kwargs=%r", scopes, kwargs) - - result = self._msal_app.acquire_token_for_client(list(scopes), **kwargs) + def acquire_token(self, scopes, **kwargs): + logger.debug("ServicePrincipalCredential.acquire_token: scopes=%r, kwargs=%r", scopes, kwargs) + result = self._msal_app.acquire_token_for_client(scopes, **kwargs) check_result(result) - return build_sdk_access_token(result) + return result class CloudShellCredential: # pylint: disable=too-few-public-methods @@ -126,12 +119,11 @@ # token_cache=... ) - def get_token(self, *scopes, **kwargs): - logger.debug("CloudShellCredential.get_token: scopes=%r, kwargs=%r", scopes, kwargs) - # kwargs is already sanitized by CredentialAdaptor, so it can be safely passed to MSAL - result = self._msal_app.acquire_token_interactive(list(scopes), prompt="none", **kwargs) + def acquire_token(self, scopes, **kwargs): + logger.debug("CloudShellCredential.acquire_token: scopes=%r, kwargs=%r", scopes, kwargs) + result = self._msal_app.acquire_token_interactive(scopes, prompt="none", **kwargs) check_result(result, scopes=scopes) - return build_sdk_access_token(result) + return result class ManagedIdentityCredential: # pylint: disable=too-few-public-methods @@ -139,14 +131,19 @@ Currently, only Azure Arc's system-assigned managed identity is supported. """ - def __init__(self): + def __init__(self, client_id=None, resource_id=None, object_id=None): import requests - self._msal_client = ManagedIdentityClient(SystemAssignedManagedIdentity(), http_client=requests.Session()) + if client_id or resource_id or object_id: + managed_identity = UserAssignedManagedIdentity( + client_id=client_id, resource_id=resource_id, object_id=object_id) + else: + managed_identity = SystemAssignedManagedIdentity() + self._msal_client = ManagedIdentityClient(managed_identity, http_client=requests.Session()) - def get_token(self, *scopes, **kwargs): - logger.debug("ManagedIdentityCredential.get_token: scopes=%r, kwargs=%r", scopes, kwargs) + def acquire_token(self, scopes, **kwargs): + logger.debug("ManagedIdentityCredential.acquire_token: scopes=%r, kwargs=%r", scopes, kwargs) from .util import scopes_to_resource result = self._msal_client.acquire_token_for_client(resource=scopes_to_resource(scopes)) check_result(result) - return build_sdk_access_token(result) + return result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/auth/util.py new/azure_cli_core-2.71.0/azure/cli/core/auth/util.py --- old/azure_cli_core-2.70.0/azure/cli/core/auth/util.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/auth/util.py 2025-03-25 10:17:55.000000000 +0100 @@ -53,7 +53,7 @@ raise AuthenticationError(error_description, msal_error=error, recommendation=recommendation) -def _generate_login_command(scopes=None, claims=None): +def _generate_login_command(scopes=None, claims_challenge=None): login_command = ['az login'] # Rejected by Conditional Access policy, like MFA @@ -61,7 +61,7 @@ login_command.append('--scope {}'.format(' '.join(scopes))) # Rejected by CAE - if claims: + if claims_challenge: # Explicit logout is needed: https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/335 return 'az logout\n' + ' '.join(login_command) @@ -140,9 +140,6 @@ def build_sdk_access_token(token_entry): - import time - request_time = int(time.time()) - # MSAL token entry sample: # { # 'access_token': 'eyJ0eXAiOiJKV...', @@ -153,7 +150,8 @@ # Importing azure.core.credentials.AccessToken is expensive. # This can slow down commands that doesn't need azure.core, like `az account get-access-token`. # So We define our own AccessToken. - return AccessToken(token_entry["access_token"], request_time + token_entry["expires_in"]) + from .constants import ACCESS_TOKEN, EXPIRES_IN + return AccessToken(token_entry[ACCESS_TOKEN], _now_timestamp() + token_entry[EXPIRES_IN]) def decode_access_token(access_token): @@ -177,3 +175,8 @@ error_template = f.read() return success_template, error_template + + +def _now_timestamp(): + import time + return int(time.time()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/breaking_change.py new/azure_cli_core-2.71.0/azure/cli/core/breaking_change.py --- old/azure_cli_core-2.70.0/azure/cli/core/breaking_change.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/breaking_change.py 2025-03-25 10:17:55.000000000 +0100 @@ -4,6 +4,7 @@ # -------------------------------------------------------------------------------------------- import abc import argparse +import re from collections import defaultdict from knack.log import get_logger @@ -14,6 +15,7 @@ logger = get_logger() NEXT_BREAKING_CHANGE_RELEASE = '2.73.0' +NEXT_BREAKING_CHANGE_DATE = 'May 2025' DEFAULT_BREAKING_CHANGE_TAG = '[Breaking Change]' @@ -24,7 +26,7 @@ if isinstance(action, type) and issubclass(action, argparse.Action): action_class = action elif isinstance(action, str): - action_class = cli_ctx.invocation.command_loader.cli_ctx.invocation.parser._registries['action'][action] # pylint: disable=protected-access + action_class = cli_ctx.invocation.parser._registries['action'][action] # pylint: disable=protected-access return action_class @@ -151,9 +153,12 @@ class NextBreakingChangeWindow(TargetVersion): def __str__(self): next_breaking_change_version = _next_breaking_change_version() + message = 'in next breaking change release' if next_breaking_change_version: - return f'in next breaking change release({next_breaking_change_version})' - return 'in next breaking change release' + message += f'({next_breaking_change_version})' + if NEXT_BREAKING_CHANGE_DATE: + message += f' scheduled for {NEXT_BREAKING_CHANGE_DATE}' + return message def version(self): return _next_breaking_change_version() @@ -172,6 +177,18 @@ # pylint: disable=too-few-public-methods +class NonVersion(TargetVersion): + def __init__(self, msg): + self._msg = msg + + def __str__(self): + return f'in {self._msg}' + + def version(self): + return None + + +# pylint: disable=too-few-public-methods class UnspecificVersion(TargetVersion): def __str__(self): return 'in a future release' @@ -192,8 +209,10 @@ self.target = target if target else '/'.join(self.args) if self.args else self.cmd if isinstance(target_version, TargetVersion): self._target_version = target_version - elif isinstance(target_version, str): + elif isinstance(target_version, str) and re.match(r'\d+.\d+.\d+', target_version): self._target_version = ExactVersion(target_version) + elif isinstance(target_version, str): + self._target_version = NonVersion(target_version) else: self._target_version = UnspecificVersion() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/profiles/_shared.py new/azure_cli_core-2.71.0/azure/cli/core/profiles/_shared.py --- old/azure_cli_core-2.70.0/azure/cli/core/profiles/_shared.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/profiles/_shared.py 2025-03-25 10:17:55.000000000 +0100 @@ -165,7 +165,7 @@ 'snapshots': '2023-10-02', 'galleries': '2021-10-01', 'gallery_images': '2021-10-01', - 'gallery_image_versions': '2023-07-03', + 'gallery_image_versions': '2024-03-03', 'gallery_applications': '2021-07-01', 'gallery_application_versions': '2022-01-03', 'shared_galleries': '2022-01-03', @@ -186,9 +186,9 @@ ResourceType.MGMT_RESOURCE_MANAGEDAPPLICATIONS: '2019-07-01', ResourceType.MGMT_NETWORK_DNS: '2018-05-01', ResourceType.MGMT_NETWORK_PRIVATEDNS: None, - ResourceType.MGMT_KEYVAULT: SDKProfile('2023-07-01', { + ResourceType.MGMT_KEYVAULT: SDKProfile('2024-11-01', { 'vaults': '2023-02-01', - 'managed_hsms': '2023-07-01' + 'managed_hsms': '2024-11-01' }), ResourceType.MGMT_AUTHORIZATION: SDKProfile('2022-04-01', { 'classic_administrators': '2015-06-01', @@ -216,7 +216,7 @@ ResourceType.DATA_STORAGE: '2018-11-09', ResourceType.DATA_STORAGE_BLOB: '2022-11-02', ResourceType.DATA_STORAGE_FILEDATALAKE: '2021-08-06', - ResourceType.DATA_STORAGE_FILESHARE: '2024-08-04', + ResourceType.DATA_STORAGE_FILESHARE: '2025-05-05', ResourceType.DATA_STORAGE_QUEUE: '2018-03-28', ResourceType.DATA_COSMOS_TABLE: '2017-04-17', ResourceType.MGMT_SERVICEBUS: '2022-10-01-preview', @@ -262,7 +262,7 @@ ResourceType.MGMT_ARO: '2023-11-22', ResourceType.MGMT_DATABOXEDGE: '2021-02-01-preview', ResourceType.MGMT_CUSTOMLOCATION: '2021-03-15-preview', - ResourceType.MGMT_CONTAINERSERVICE: SDKProfile('2024-10-01'), + ResourceType.MGMT_CONTAINERSERVICE: SDKProfile('2025-01-01'), ResourceType.MGMT_APPCONTAINERS: '2022-10-01', }, '2020-09-01-hybrid': { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure/cli/core/util.py new/azure_cli_core-2.71.0/azure/cli/core/util.py --- old/azure_cli_core-2.70.0/azure/cli/core/util.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure/cli/core/util.py 2025-03-25 10:17:55.000000000 +0100 @@ -256,8 +256,8 @@ # pylint: disable=inconsistent-return-statements def empty_on_404(ex): - from msrestazure.azure_exceptions import CloudError - if isinstance(ex, CloudError) and ex.status_code == 404: + from azure.core.exceptions import HttpResponseError + if isinstance(ex, HttpResponseError) and ex.status_code == 404: return None raise ex diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure_cli_core.egg-info/PKG-INFO new/azure_cli_core-2.71.0/azure_cli_core.egg-info/PKG-INFO --- old/azure_cli_core-2.70.0/azure_cli_core.egg-info/PKG-INFO 2025-02-26 07:24:39.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure_cli_core.egg-info/PKG-INFO 2025-03-25 10:18:47.000000000 +0100 @@ -1,6 +1,6 @@ -Metadata-Version: 2.2 +Metadata-Version: 2.4 Name: azure-cli-core -Version: 2.70.0 +Version: 2.71.0 Summary: Microsoft Azure Command-Line Tools Core Module Home-page: https://github.com/Azure/azure-cli Author: Microsoft Corporation @@ -26,7 +26,7 @@ Requires-Dist: humanfriendly~=10.0 Requires-Dist: jmespath Requires-Dist: knack~=0.11.0 -Requires-Dist: microsoft-security-utilities-secret-masker~=1.0.0b2 +Requires-Dist: microsoft-security-utilities-secret-masker~=1.0.0b4 Requires-Dist: msal-extensions==1.2.0 Requires-Dist: msal[broker]==1.31.2b1 Requires-Dist: msrestazure~=0.6.4 @@ -43,6 +43,7 @@ Dynamic: description Dynamic: home-page Dynamic: license +Dynamic: license-file Dynamic: requires-dist Dynamic: requires-python Dynamic: summary diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/azure_cli_core.egg-info/requires.txt new/azure_cli_core-2.71.0/azure_cli_core.egg-info/requires.txt --- old/azure_cli_core-2.70.0/azure_cli_core.egg-info/requires.txt 2025-02-26 07:24:39.000000000 +0100 +++ new/azure_cli_core-2.71.0/azure_cli_core.egg-info/requires.txt 2025-03-25 10:18:47.000000000 +0100 @@ -5,7 +5,7 @@ humanfriendly~=10.0 jmespath knack~=0.11.0 -microsoft-security-utilities-secret-masker~=1.0.0b2 +microsoft-security-utilities-secret-masker~=1.0.0b4 msal-extensions==1.2.0 msal[broker]==1.31.2b1 msrestazure~=0.6.4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/azure_cli_core-2.70.0/setup.py new/azure_cli_core-2.71.0/setup.py --- old/azure_cli_core-2.70.0/setup.py 2025-02-26 07:24:09.000000000 +0100 +++ new/azure_cli_core-2.71.0/setup.py 2025-03-25 10:17:55.000000000 +0100 @@ -8,7 +8,7 @@ from codecs import open from setuptools import setup, find_packages -VERSION = "2.70.0" +VERSION = "2.71.0" # If we have source, validate that our version numbers match # This should prevent uploading releases with mismatched versions. @@ -52,7 +52,7 @@ 'humanfriendly~=10.0', 'jmespath', 'knack~=0.11.0', - 'microsoft-security-utilities-secret-masker~=1.0.0b2', + 'microsoft-security-utilities-secret-masker~=1.0.0b4', 'msal-extensions==1.2.0', 'msal[broker]==1.31.2b1', 'msrestazure~=0.6.4',