From 1c008b815083407bbdcb366ac2a7e7d2fc79571c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:45:27 +0000 Subject: [PATCH 01/17] Initial plan From aa2bfbdd3c2b6b9864ec467cf08447ba6e8e4dcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:47:02 +0000 Subject: [PATCH 02/17] Allow Azure Arc managed identity selectors --- msal/managed_identity.py | 22 +++++++++++--------- tests/test_mi.py | 45 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/msal/managed_identity.py b/msal/managed_identity.py index b2fc446c..b2f20712 100644 --- a/msal/managed_identity.py +++ b/msal/managed_identity.py @@ -452,13 +452,8 @@ def _obtain_token( ) arc_endpoint = _get_arc_endpoint() if arc_endpoint: - if ManagedIdentity.is_user_assigned(managed_identity): - raise ManagedIdentityError( # Note: Azure Identity for Python raised exception too - "Invalid managed_identity parameter. " - "Azure Arc supports only system-assigned managed identity, " - "See also " - "https://learn.microsoft.com/en-us/azure/service-fabric/configure-existing-cluster-enable-managed-identity-token-service") - return _obtain_token_on_arc(http_client, arc_endpoint, resource) + return _obtain_token_on_arc( + http_client, arc_endpoint, resource, managed_identity) return _obtain_token_on_azure_vm(http_client, managed_identity, resource) @@ -643,12 +638,19 @@ def _obtain_token_on_service_fabric( class ArcPlatformNotSupportedError(ManagedIdentityError): pass -def _obtain_token_on_arc(http_client, endpoint, resource): +def _obtain_token_on_arc(http_client, endpoint, resource, managed_identity=None): # https://learn.microsoft.com/en-us/azure/azure-arc/servers/managed-identity-authentication logger.debug("Obtaining token via managed identity on Azure Arc") + params = {"api-version": "2020-06-01", "resource": resource} + if managed_identity: + _adjust_param(params, managed_identity, types_mapping={ + ManagedIdentity.CLIENT_ID: "client_id", + ManagedIdentity.RESOURCE_ID: "mi_res_id", + ManagedIdentity.OBJECT_ID: "object_id", + }) resp = http_client.get( endpoint, - params={"api-version": "2020-06-01", "resource": resource}, + params=params.copy(), headers={"Metadata": "true"}, ) www_auth = "www-authenticate" # Header in lower case @@ -674,7 +676,7 @@ def _obtain_token_on_arc(http_client, endpoint, resource): secret = f.read() response = http_client.get( endpoint, - params={"api-version": "2020-06-01", "resource": resource}, + params=params.copy(), headers={"Metadata": "true", "Authorization": "Basic {}".format(secret)}, ) try: diff --git a/tests/test_mi.py b/tests/test_mi.py index e9fc6d00..73e1f81f 100644 --- a/tests/test_mi.py +++ b/tests/test_mi.py @@ -477,6 +477,50 @@ def test_arc_error_should_be_normalized(self, mocked_stat): if sys.platform in _supported_arc_platforms_and_their_prefixes: self.fail("Should not raise ArcPlatformNotSupportedError") + def _assert_user_assigned_selector(self, managed_identity, selector_name, selector_value): + app = ManagedIdentityClient(managed_identity, http_client=requests.Session()) + with patch.object(app._http_client, "get", side_effect=[ + self.challenge, + MinimalResponse( + status_code=200, + text='{"access_token": "AT", "expires_in": "1234", "resource": "R"}', + ), + ]) as mocked_method: + try: + result = app.acquire_token_for_client(resource="R") + self.assertEqual("AT", result["access_token"]) + expected_params = { + "api-version": "2020-06-01", + "resource": "R", + selector_name: selector_value, + } + self.assertEqual(expected_params, mocked_method.call_args_list[0].kwargs["params"]) + self.assertEqual(expected_params, mocked_method.call_args_list[1].kwargs["params"]) + except ArcPlatformNotSupportedError: + if sys.platform in _supported_arc_platforms_and_their_prefixes: + self.fail("Should not raise ArcPlatformNotSupportedError") + + def test_arc_user_assigned_client_id_should_be_forwarded(self, mocked_stat): + self._assert_user_assigned_selector( + UserAssignedManagedIdentity(client_id="client-id"), + "client_id", + "client-id", + ) + + def test_arc_user_assigned_resource_id_should_be_forwarded_as_mi_res_id(self, mocked_stat): + self._assert_user_assigned_selector( + UserAssignedManagedIdentity(resource_id="resource-id"), + "mi_res_id", + "resource-id", + ) + + def test_arc_user_assigned_object_id_should_be_forwarded(self, mocked_stat): + self._assert_user_assigned_selector( + UserAssignedManagedIdentity(object_id="object-id"), + "object_id", + "object-id", + ) + class GetManagedIdentitySourceTestCase(unittest.TestCase): @@ -531,4 +575,3 @@ def test_cloud_shell(self): def test_default_to_vm(self): self.assertEqual(get_managed_identity_source(), DEFAULT_TO_VM) - From 2e1f3417dab319f7cbb888905f771cb29cd89a97 Mon Sep 17 00:00:00 2001 From: Nilesh Choudhary Date: Thu, 16 Jul 2026 15:34:53 +0100 Subject: [PATCH 03/17] Bump version to 1.38.0rc2 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 593fce2a-8559-422e-92e2-46fb29c251a4 --- msal/sku.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msal/sku.py b/msal/sku.py index 5e58eda9..6e4e9528 100644 --- a/msal/sku.py +++ b/msal/sku.py @@ -2,5 +2,5 @@ """ # The __init__.py will import this. Not the other way around. -__version__ = "1.37.0" +__version__ = "1.38.0rc2" SKU = "MSAL.Python" From e4769c1fd03c0011f3cdf9c9975b9b50bbda51ca Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 11:09:16 -0700 Subject: [PATCH 04/17] Fix pipeline container --- .Pipelines/pipeline-publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index e4d709b2..1724f8c0 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -36,6 +36,7 @@ pr: none variables: CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] system.debug: ${{ parameters.debug }} + WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' # https://aka.ms/obpipelines/containers resources: repositories: From c6faa486fa44e96667c3fe5bbaab6a19c1779ca2 Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 11:27:34 -0700 Subject: [PATCH 05/17] Fix pipeline host --- .Pipelines/pipeline-publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 1724f8c0..da70b44c 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -65,6 +65,7 @@ extends: tsaEnabled: true featureFlags: EnableCDPxPAT: false + WindowsHostVersion: '1ESWindows2022' stages: From 8b3ee01c53006e1f45489623e9914b3ac5396031 Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 11:53:22 -0700 Subject: [PATCH 06/17] Fix pipeline artifact management --- .Pipelines/pipeline-publish.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index da70b44c..b04f71dc 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -137,7 +137,8 @@ extends: - task: DownloadPipelineArtifact@2 displayName: 'Download build artifact' inputs: - artifactName: drop_Build_BuildDist + buildType: 'current' + itemPattern: '**/drop_build_builddist/*' targetPath: $(Pipeline.Workspace)/python-dist - task: UsePythonVersion@0 @@ -213,7 +214,8 @@ extends: - task: DownloadPipelineArtifact@2 displayName: 'Download python-dist artifact' inputs: - artifactName: drop_Build_BuildDist + buildType: 'current' + itemPattern: '**/drop_build_builddist/*' targetPath: $(Pipeline.Workspace)/python-dist - task: EsrpRelease@12 @@ -227,7 +229,7 @@ extends: intent: 'PackageDistribution' contenttype: 'PyPi' contentsource: 'Folder' - folderlocation: '$(Pipeline.Workspace)/python-dist' + folderlocation: '$(Pipeline.Workspace)/python-dist/drop_build_builddist' waitforreleasecompletion: true owners: 'ryauld@microsoft.com,avdunn@microsoft.com' approvers: 'avdunn@microsoft.com,bogavril@microsoft.com' From 5a5e790cac59509ed06fa49db78d0b6de78c0bd3 Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 12:26:04 -0700 Subject: [PATCH 07/17] Fix pipline artifact uploads --- .Pipelines/pipeline-publish.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index b04f71dc..a24e80a5 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -113,6 +113,12 @@ extends: cp -r dist/* $(ob_outputDirectory)/ displayName: 'Copy dist to output directory' + - task: PublishPipelineArtifact@1 + displayName: 'Publish build artifacts' + inputs: + targetPath: '$(ob_outputDirectory)' + artifactName: 'drop_build_builddist' + # ======================================================================== # Stage 5a - Publish to test.pypi.org (Preview / RC) # ======================================================================== @@ -138,7 +144,7 @@ extends: displayName: 'Download build artifact' inputs: buildType: 'current' - itemPattern: '**/drop_build_builddist/*' + artifactName: 'drop_build_builddist' targetPath: $(Pipeline.Workspace)/python-dist - task: UsePythonVersion@0 @@ -215,7 +221,7 @@ extends: displayName: 'Download python-dist artifact' inputs: buildType: 'current' - itemPattern: '**/drop_build_builddist/*' + artifactName: 'drop_build_builddist' targetPath: $(Pipeline.Workspace)/python-dist - task: EsrpRelease@12 @@ -229,7 +235,7 @@ extends: intent: 'PackageDistribution' contenttype: 'PyPi' contentsource: 'Folder' - folderlocation: '$(Pipeline.Workspace)/python-dist/drop_build_builddist' + folderlocation: '$(Pipeline.Workspace)/python-dist' waitforreleasecompletion: true owners: 'ryauld@microsoft.com,avdunn@microsoft.com' approvers: 'avdunn@microsoft.com,bogavril@microsoft.com' From 9a0a7dbe9f42d881bcc6a662daee188d8c6ac8d0 Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 12:33:57 -0700 Subject: [PATCH 08/17] Fix pipeline Linux task --- .Pipelines/pipeline-publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index a24e80a5..349e7178 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -117,7 +117,7 @@ extends: displayName: 'Publish build artifacts' inputs: targetPath: '$(ob_outputDirectory)' - artifactName: 'drop_build_builddist' + artifactName: 'drop_Build_BuildDist' # ======================================================================== # Stage 5a - Publish to test.pypi.org (Preview / RC) @@ -144,7 +144,7 @@ extends: displayName: 'Download build artifact' inputs: buildType: 'current' - artifactName: 'drop_build_builddist' + artifactName: 'drop_Build_BuildDist' targetPath: $(Pipeline.Workspace)/python-dist - task: UsePythonVersion@0 @@ -221,7 +221,7 @@ extends: displayName: 'Download python-dist artifact' inputs: buildType: 'current' - artifactName: 'drop_build_builddist' + artifactName: 'drop_Build_BuildDist' targetPath: $(Pipeline.Workspace)/python-dist - task: EsrpRelease@12 From 9bd0017f471f72124e1678112afdd44dbe650f9f Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 12:43:24 -0700 Subject: [PATCH 09/17] Refactor the Linux stuff --- .Pipelines/pipeline-publish.yml | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 349e7178..a1bd2060 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -87,38 +87,30 @@ extends: displayName: 'Build sdist + wheel (Python 3.12)' pool: type: linux - isCustom: true - vmImage: 'ubuntu-22.04' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' + LinuxContainerImage: 'onebranch.azurecr.io/linux/ubuntu-2204:latest' steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: '3.12' - displayName: 'Use Python 3.12' + - script: | + apt-get update && apt-get install -y python3 python3-pip python3-venv + displayName: 'Install Python 3' - script: | - python -m pip install --upgrade pip build twine + python3 -m pip install --upgrade pip build twine displayName: 'Install build toolchain' - script: | - python -m build + python3 -m build displayName: 'Build sdist and wheel' - script: | - python -m twine check dist/* + python3 -m twine check dist/* displayName: 'Verify distribution (twine check)' - script: | cp -r dist/* $(ob_outputDirectory)/ displayName: 'Copy dist to output directory' - - task: PublishPipelineArtifact@1 - displayName: 'Publish build artifacts' - inputs: - targetPath: '$(ob_outputDirectory)' - artifactName: 'drop_Build_BuildDist' - # ======================================================================== # Stage 5a - Publish to test.pypi.org (Preview / RC) # ======================================================================== @@ -144,7 +136,7 @@ extends: displayName: 'Download build artifact' inputs: buildType: 'current' - artifactName: 'drop_Build_BuildDist' + itemPattern: '**/drop_Build_BuildDist/*' targetPath: $(Pipeline.Workspace)/python-dist - task: UsePythonVersion@0 @@ -221,7 +213,7 @@ extends: displayName: 'Download python-dist artifact' inputs: buildType: 'current' - artifactName: 'drop_Build_BuildDist' + itemPattern: '**/drop_Build_BuildDist/*' targetPath: $(Pipeline.Workspace)/python-dist - task: EsrpRelease@12 @@ -235,7 +227,7 @@ extends: intent: 'PackageDistribution' contenttype: 'PyPi' contentsource: 'Folder' - folderlocation: '$(Pipeline.Workspace)/python-dist' + folderlocation: '$(Pipeline.Workspace)/python-dist/drop_Build_BuildDist' waitforreleasecompletion: true owners: 'ryauld@microsoft.com,avdunn@microsoft.com' approvers: 'avdunn@microsoft.com,bogavril@microsoft.com' From 858f41ab6f2cfba639b59ce3aa52e88de6b92b06 Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 13:21:53 -0700 Subject: [PATCH 10/17] Fix pipeline python install --- .Pipelines/pipeline-publish.yml | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index a1bd2060..a2cef2cc 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -84,32 +84,32 @@ extends: condition: eq(dependencies.E2ETests.result, 'Succeeded') jobs: - job: BuildDist - displayName: 'Build sdist + wheel (Python 3.12)' + displayName: 'Build sdist + wheel' pool: - type: linux + type: windows variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' - LinuxContainerImage: 'onebranch.azurecr.io/linux/ubuntu-2204:latest' steps: - - script: | - apt-get update && apt-get install -y python3 python3-pip python3-venv - displayName: 'Install Python 3' + - script: python --version + displayName: 'Check Python version' - - script: | - python3 -m pip install --upgrade pip build twine + - script: python -m pip install --upgrade pip build twine displayName: 'Install build toolchain' - - script: | - python3 -m build + - script: python -m build displayName: 'Build sdist and wheel' - - script: | - python3 -m twine check dist/* + - script: python -m twine check dist\* displayName: 'Verify distribution (twine check)' - - script: | - cp -r dist/* $(ob_outputDirectory)/ + - task: CopyFiles@2 displayName: 'Copy dist to output directory' + inputs: + SourceFolder: '$(Build.SourcesDirectory)\dist' + Contents: | + *.whl + *.tar.gz + TargetFolder: '$(ob_outputDirectory)' # ======================================================================== # Stage 5a - Publish to test.pypi.org (Preview / RC) @@ -136,8 +136,8 @@ extends: displayName: 'Download build artifact' inputs: buildType: 'current' - itemPattern: '**/drop_Build_BuildDist/*' - targetPath: $(Pipeline.Workspace)/python-dist + artifactName: 'drop_Build_BuildDist' + targetPath: $(Pipeline.Workspace)\python-dist - task: UsePythonVersion@0 inputs: @@ -213,8 +213,8 @@ extends: displayName: 'Download python-dist artifact' inputs: buildType: 'current' - itemPattern: '**/drop_Build_BuildDist/*' - targetPath: $(Pipeline.Workspace)/python-dist + artifactName: 'drop_Build_BuildDist' + targetPath: $(Pipeline.Workspace)\python-dist - task: EsrpRelease@12 displayName: 'Publish to PyPI via ESRP' @@ -227,7 +227,7 @@ extends: intent: 'PackageDistribution' contenttype: 'PyPi' contentsource: 'Folder' - folderlocation: '$(Pipeline.Workspace)/python-dist/drop_Build_BuildDist' + folderlocation: '$(Pipeline.Workspace)\python-dist' waitforreleasecompletion: true owners: 'ryauld@microsoft.com,avdunn@microsoft.com' approvers: 'avdunn@microsoft.com,bogavril@microsoft.com' From 1e4f06304a0a6c0d3a08a3710b78ae2a59dd3c1f Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 13:45:40 -0700 Subject: [PATCH 11/17] Fix pipeline python install --- .Pipelines/pipeline-publish.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index a2cef2cc..ffb5b4af 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -66,6 +66,8 @@ extends: featureFlags: EnableCDPxPAT: false WindowsHostVersion: '1ESWindows2022' + networkisolation: + policy: Permissive stages: @@ -93,15 +95,12 @@ extends: - script: python --version displayName: 'Check Python version' - - script: python -m pip install --upgrade pip build twine + - script: python -m pip install --upgrade pip build displayName: 'Install build toolchain' - script: python -m build displayName: 'Build sdist and wheel' - - script: python -m twine check dist\* - displayName: 'Verify distribution (twine check)' - - task: CopyFiles@2 displayName: 'Copy dist to output directory' inputs: From 21be29d1c077f1b664fbc69f4ca8adb2eeefd238 Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 14:05:35 -0700 Subject: [PATCH 12/17] Remove unnecessary Python install --- .Pipelines/pipeline-publish.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index ffb5b4af..17de5b40 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -66,8 +66,6 @@ extends: featureFlags: EnableCDPxPAT: false WindowsHostVersion: '1ESWindows2022' - networkisolation: - policy: Permissive stages: @@ -95,10 +93,7 @@ extends: - script: python --version displayName: 'Check Python version' - - script: python -m pip install --upgrade pip build - displayName: 'Install build toolchain' - - - script: python -m build + - script: python setup.py sdist bdist_wheel displayName: 'Build sdist and wheel' - task: CopyFiles@2 From e49fb76d7b5b401cfd6d16eba9ce93837cb088fe Mon Sep 17 00:00:00 2001 From: avdunn Date: Thu, 16 Jul 2026 14:29:02 -0700 Subject: [PATCH 13/17] Update release owners --- .Pipelines/pipeline-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 17de5b40..18d4767d 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -223,7 +223,7 @@ extends: contentsource: 'Folder' folderlocation: '$(Pipeline.Workspace)\python-dist' waitforreleasecompletion: true - owners: 'ryauld@microsoft.com,avdunn@microsoft.com' + owners: 'avdunn@microsoft.com,bogavril@microsoft.com' approvers: 'avdunn@microsoft.com,bogavril@microsoft.com' serviceendpointurl: 'https://api.esrp.microsoft.com' mainpublisher: 'ESRPRELPACMAN' From 67269a5f3dd86f000a215a6a8f075dcfa573a30c Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:58:02 -0700 Subject: [PATCH 14/17] Fix Azure Arc resource-id UAMI to forward msi_res_id (not mi_res_id) Live testing against the Azure Connected Machine agent (1.67, user-assigned preview) showed the Arc token endpoint only honors the IMDS 'msi_res_id' spelling for the resource-id selector; 'mi_res_id' is silently ignored and returns the system-assigned identity, so a resource-id UAMI never resolved. Forward 'msi_res_id' on the Arc request (matching the VM/IMDS source). Update the unit test accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c --- msal/managed_identity.py | 2 +- tests/test_mi.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/msal/managed_identity.py b/msal/managed_identity.py index b2f20712..afefd928 100644 --- a/msal/managed_identity.py +++ b/msal/managed_identity.py @@ -645,7 +645,7 @@ def _obtain_token_on_arc(http_client, endpoint, resource, managed_identity=None) if managed_identity: _adjust_param(params, managed_identity, types_mapping={ ManagedIdentity.CLIENT_ID: "client_id", - ManagedIdentity.RESOURCE_ID: "mi_res_id", + ManagedIdentity.RESOURCE_ID: "msi_res_id", # Azure Arc honors the IMDS msi_res_id spelling; mi_res_id is ignored and returns the system-assigned identity ManagedIdentity.OBJECT_ID: "object_id", }) resp = http_client.get( diff --git a/tests/test_mi.py b/tests/test_mi.py index 73e1f81f..beb7926a 100644 --- a/tests/test_mi.py +++ b/tests/test_mi.py @@ -507,10 +507,10 @@ def test_arc_user_assigned_client_id_should_be_forwarded(self, mocked_stat): "client-id", ) - def test_arc_user_assigned_resource_id_should_be_forwarded_as_mi_res_id(self, mocked_stat): + def test_arc_user_assigned_resource_id_should_be_forwarded_as_msi_res_id(self, mocked_stat): self._assert_user_assigned_selector( UserAssignedManagedIdentity(resource_id="resource-id"), - "mi_res_id", + "msi_res_id", "resource-id", ) From a9a7689053d26dac89bed431657efc7c2852d6d1 Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:00:31 -0700 Subject: [PATCH 15/17] Revert out-of-scope publish-pipeline changes; keep PR focused on Azure Arc MI The publish-pipeline edits (BuildDist Linux->Windows pool, python -m build -> setup.py, dropped twine check, Windows-style artifact paths on Linux pools) are unrelated to the Azure Arc managed identity selector forwarding this PR targets and were flagged in review as non-deterministic and broken on Linux. Restore .Pipelines/pipeline-publish.yml to the dev baseline so those concerns are removed and the PR only carries the Arc change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c --- .Pipelines/pipeline-publish.yml | 48 ++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/.Pipelines/pipeline-publish.yml b/.Pipelines/pipeline-publish.yml index 18d4767d..e4d709b2 100644 --- a/.Pipelines/pipeline-publish.yml +++ b/.Pipelines/pipeline-publish.yml @@ -36,7 +36,6 @@ pr: none variables: CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] system.debug: ${{ parameters.debug }} - WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' # https://aka.ms/obpipelines/containers resources: repositories: @@ -65,7 +64,6 @@ extends: tsaEnabled: true featureFlags: EnableCDPxPAT: false - WindowsHostVersion: '1ESWindows2022' stages: @@ -84,26 +82,34 @@ extends: condition: eq(dependencies.E2ETests.result, 'Succeeded') jobs: - job: BuildDist - displayName: 'Build sdist + wheel' + displayName: 'Build sdist + wheel (Python 3.12)' pool: - type: windows + type: linux + isCustom: true + vmImage: 'ubuntu-22.04' variables: ob_outputDirectory: '$(Build.ArtifactStagingDirectory)' steps: - - script: python --version - displayName: 'Check Python version' + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.12' + displayName: 'Use Python 3.12' - - script: python setup.py sdist bdist_wheel + - script: | + python -m pip install --upgrade pip build twine + displayName: 'Install build toolchain' + + - script: | + python -m build displayName: 'Build sdist and wheel' - - task: CopyFiles@2 + - script: | + python -m twine check dist/* + displayName: 'Verify distribution (twine check)' + + - script: | + cp -r dist/* $(ob_outputDirectory)/ displayName: 'Copy dist to output directory' - inputs: - SourceFolder: '$(Build.SourcesDirectory)\dist' - Contents: | - *.whl - *.tar.gz - TargetFolder: '$(ob_outputDirectory)' # ======================================================================== # Stage 5a - Publish to test.pypi.org (Preview / RC) @@ -129,9 +135,8 @@ extends: - task: DownloadPipelineArtifact@2 displayName: 'Download build artifact' inputs: - buildType: 'current' - artifactName: 'drop_Build_BuildDist' - targetPath: $(Pipeline.Workspace)\python-dist + artifactName: drop_Build_BuildDist + targetPath: $(Pipeline.Workspace)/python-dist - task: UsePythonVersion@0 inputs: @@ -206,9 +211,8 @@ extends: - task: DownloadPipelineArtifact@2 displayName: 'Download python-dist artifact' inputs: - buildType: 'current' - artifactName: 'drop_Build_BuildDist' - targetPath: $(Pipeline.Workspace)\python-dist + artifactName: drop_Build_BuildDist + targetPath: $(Pipeline.Workspace)/python-dist - task: EsrpRelease@12 displayName: 'Publish to PyPI via ESRP' @@ -221,9 +225,9 @@ extends: intent: 'PackageDistribution' contenttype: 'PyPi' contentsource: 'Folder' - folderlocation: '$(Pipeline.Workspace)\python-dist' + folderlocation: '$(Pipeline.Workspace)/python-dist' waitforreleasecompletion: true - owners: 'avdunn@microsoft.com,bogavril@microsoft.com' + owners: 'ryauld@microsoft.com,avdunn@microsoft.com' approvers: 'avdunn@microsoft.com,bogavril@microsoft.com' serviceendpointurl: 'https://api.esrp.microsoft.com' mainpublisher: 'ESRPRELPACMAN' From 1226380ce91c528e189a499f14346b6c5d53d7cd Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:21:11 -0700 Subject: [PATCH 16/17] Fail closed when Azure Arc does not confirm the requested user-assigned identity Mirrors the response verification already present in MSAL .NET (VerifyUserAssignedIdentityWasHonored) and Go (verifyAzureArcUserAssignedIdentity), addressing review feedback that the Python PR only forwarded the selector without verifying the token response. A legacy Azure Arc agent ignores the client_id/object_id/msi_res_id selector and silently returns the machine's system-assigned identity. After a 200, compare the echoed identity (client_id/object_id/msi_res_id; mi_res_id accepted as a safety net) to the requested selector and raise ManagedIdentityError if missing or mismatched, so MSAL never returns a token for a different identity than the one requested. System-assigned is unaffected. Existing UAMI forwarding tests now echo the identity; added a fail-closed test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c --- msal/managed_identity.py | 28 ++++++++++++++++++++++++++++ tests/test_mi.py | 24 +++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/msal/managed_identity.py b/msal/managed_identity.py index afefd928..b045c971 100644 --- a/msal/managed_identity.py +++ b/msal/managed_identity.py @@ -638,6 +638,32 @@ def _obtain_token_on_service_fabric( class ArcPlatformNotSupportedError(ManagedIdentityError): pass +def _raise_if_arc_did_not_honor_user_assigned_identity(managed_identity, payload): + """Fail closed when a user-assigned identity was requested but Azure Arc did not confirm it. + + A legacy Azure Arc agent ignores the client_id / object_id / msi_res_id selector and silently + returns the machine's system-assigned identity. An agent that supports user-assigned managed + identity echoes the identity it used in the token response. When that echo is missing or does + not match the requested selector, MSAL must not hand back a token for a different identity than + the one that was requested. + """ + if not ManagedIdentity.is_user_assigned(managed_identity): + return # System-assigned: there is no requested identity to confirm + requested = managed_identity.get(ManagedIdentity.ID) + echoed = { + ManagedIdentity.CLIENT_ID: payload.get("client_id"), + ManagedIdentity.OBJECT_ID: payload.get("object_id"), + # Azure Arc echoes msi_res_id; accept the mi_res_id spelling too as a safety net + ManagedIdentity.RESOURCE_ID: payload.get("msi_res_id") or payload.get("mi_res_id"), + }.get(managed_identity.get(ManagedIdentity.ID_TYPE)) + # Compare case-insensitively: client_id / object_id are GUIDs, and an ARM resource id + # (msi_res_id) can legitimately differ in segment casing. + if not echoed or str(echoed).lower() != str(requested).lower(): + raise ManagedIdentityError( + "Azure Arc did not confirm the requested user-assigned managed identity " + "in the token response. The agent likely does not support user-assigned " + "managed identities and returned the system-assigned identity.") + def _obtain_token_on_arc(http_client, endpoint, resource, managed_identity=None): # https://learn.microsoft.com/en-us/azure/azure-arc/servers/managed-identity-authentication logger.debug("Obtaining token via managed identity on Azure Arc") @@ -683,6 +709,8 @@ def _obtain_token_on_arc(http_client, endpoint, resource, managed_identity=None) payload = json.loads(response.text) if payload.get("access_token") and payload.get("expires_in"): # Example: https://learn.microsoft.com/en-us/azure/azure-arc/servers/media/managed-identity-authentication/bash-token-output-example.png + _raise_if_arc_did_not_honor_user_assigned_identity( + managed_identity, payload) return { "access_token": payload["access_token"], "expires_in": int(payload["expires_in"]), diff --git a/tests/test_mi.py b/tests/test_mi.py index beb7926a..81dedeba 100644 --- a/tests/test_mi.py +++ b/tests/test_mi.py @@ -483,7 +483,9 @@ def _assert_user_assigned_selector(self, managed_identity, selector_name, select self.challenge, MinimalResponse( status_code=200, - text='{"access_token": "AT", "expires_in": "1234", "resource": "R"}', + # A compliant Azure Arc agent echoes the identity it used; MSAL verifies it (fail closed). + text='{"access_token": "AT", "expires_in": "1234", "resource": "R", "%s": "%s"}' % ( + selector_name, selector_value), ), ]) as mocked_method: try: @@ -521,6 +523,26 @@ def test_arc_user_assigned_object_id_should_be_forwarded(self, mocked_stat): "object-id", ) + def test_arc_user_assigned_identity_not_confirmed_should_fail_closed(self, mocked_stat): + # A legacy Azure Arc agent ignores the selector and returns the system-assigned identity: + # the token response echoes a different identity than the one requested. MSAL must fail + # closed rather than hand back a token for a different identity than requested. + app = ManagedIdentityClient( + UserAssignedManagedIdentity(client_id="client-id"), + http_client=requests.Session()) + with patch.object(app._http_client, "get", side_effect=[ + self.challenge, + MinimalResponse( + status_code=200, + text='{"access_token": "AT", "expires_in": "1234", "resource": "R", "client_id": "a-different-id"}', + ), + ]): + with self.assertRaises(ManagedIdentityError): + app.acquire_token_for_client(resource="R") + self.assertEqual( + {}, app._token_cache._cache, + "An unconfirmed identity token must not be cached") + class GetManagedIdentitySourceTestCase(unittest.TestCase): From 24f7a92a72b5bfc3ebe9a09b9114a5594e2fa5ec Mon Sep 17 00:00:00 2001 From: Gladwin Johnson <90415114+gladjohn@users.noreply.github.com> Date: Tue, 11 Aug 2026 07:16:38 -0700 Subject: [PATCH 17/17] Update version from 1.38.0rc2 to 1.38.0 --- msal/sku.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/msal/sku.py b/msal/sku.py index 6e4e9528..f3c7f97d 100644 --- a/msal/sku.py +++ b/msal/sku.py @@ -2,5 +2,5 @@ """ # The __init__.py will import this. Not the other way around. -__version__ = "1.38.0rc2" +__version__ = "1.38.0" SKU = "MSAL.Python"