Skip to content

Add managed identity E2E tests + self-hosted CI stages (MSAL Go #641 parity) - #948

Open
gladjohn wants to merge 3 commits into
devfrom
gladjohn/managed-identity-e2e-tests
Open

Add managed identity E2E tests + self-hosted CI stages (MSAL Go #641 parity)#948
gladjohn wants to merge 3 commits into
devfrom
gladjohn/managed-identity-e2e-tests

Conversation

@gladjohn

@gladjohn gladjohn commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary — Phase 1: managed identity E2E tests in CI

Mirrors the MSAL Go MI E2E setup (#641) for MSAL Python: add real managed-identity E2E tests and run them in CI on the self-hosted lab machines. This is the infrastructure step; behavior changes (e.g. Azure Arc UAMI) will follow separately.

Test — tests/test_mi_e2e.py

Real token acquisition against https://management.azure.com, asserting token_source goes identity_providercache on the second call (the Python equivalent of Go's acquireTokenTwiceAssertCaching):

  • IMDS (ManagedIdentityImdsE2ETestCase): system-assigned + user-assigned by client id / resource id / object id.
  • Azure Arc (ManagedIdentityAzureArcE2ETestCase): system-assigned only.

Uses the same lab identities and ARM resource as the Go / .NET MI E2E tests, so all SDKs exercise the same lab configuration on the same machines. The tests self-skip off-pool (IMDS gated on MSAL_TEST_MI_IMDS; Arc on Arc source detection), so they're safe on hosted agents and locally.

Pipeline — .Pipelines/template-pipeline-stages.yml

Two OneBranch stages on the self-hosted pools, mirroring Go's MI E2E – IMDS / MI E2E – Azure Arc:

  • MI E2E - IMDS → pool MISEManagedIdentity (Windows Azure VM); sets MSAL_TEST_MI_IMDS.
  • MI E2E - Azure Arc → pool MISEAZUREARC (Azure Arc machine).

No lab certificate is needed (the machine's own managed identity is used). test_mi_e2e.py is also excluded from the unit stage (consistent with the other *_e2e.py files).

Assumptions to validate on the pools

  • Python 3.x is on PATH on both self-hosted pools.
  • pip install can reach PyPI. Note: as observed with MSAL Go, the Arc machine's egress inspection can reset TLS for package downloads — if pip install can't reach PyPI on MISEAZUREARC, we'll pre-provision dependencies / use an offline wheel cache and drop the install step (documented inline in the stage).

Not in this PR (Phase 2)

The Azure Arc user-assigned managed identity fail-closed behavior (ported to MSAL.NET #6128 and MSAL Go #644) is a separate follow-up.

Mirror the MSAL Go MI E2E setup (#641). Add tests/test_mi_e2e.py, which acquires
ARM tokens for the system-assigned identity and each user-assigned binding
(client id / resource id / object id) over IMDS, plus the system-assigned identity
over Azure Arc, asserting the first call reaches the identity provider and the
second is served from the cache (token_source). Uses the same lab identities as
the Go / .NET MI E2E tests.

Wire two OneBranch stages that run the test on the self-hosted lab pools
(MISEManagedIdentity VM/IMDS and MISEAZUREARC), gated so IMDS cases run only on
the IMDS pool (MSAL_TEST_MI_IMDS) and the Arc case only on an Arc machine; the
test self-skips everywhere else. Exclude the E2E file from the unit stage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
Copilot AI review requested due to automatic review settings August 2, 2026 13:31
@gladjohn
gladjohn requested a review from a team as a code owner August 2, 2026 13:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces real end-to-end (E2E) Managed Identity (MI) token acquisition tests for MSAL Python and wires them into the shared Azure DevOps pipeline template via two new self-hosted stages (IMDS VM + Azure Arc), aiming for parity with the MSAL Go MI E2E setup.

Changes:

  • Added tests/test_mi_e2e.py with MI E2E test cases that acquire an ARM token twice and assert cache behavior via token_source.
  • Excluded the new MI E2E file from the unit-test stage.
  • Added two new self-hosted pipeline stages to run the MI E2E tests on the appropriate pools.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
tests/test_mi_e2e.py Adds real MI E2E tests for IMDS (system + UAMI variants) and Azure Arc (system only), including cache assertions.
.Pipelines/template-pipeline-stages.yml Excludes MI E2E tests from unit stage and adds two self-hosted stages to execute MI E2E tests and publish results.
Suppressed comments (1)

.Pipelines/template-pipeline-stages.yml:355

  • Same as the IMDS MI stage: this stage will also run in runPublish (release) builds because it only checks UnitTests success. If these MI E2E stages are intended for CI parity runs only, gate them behind not(${{ parameters.runPublish }}) to avoid adding self-hosted pool requirements to publish pipelines.
  displayName: 'MI E2E - Azure Arc'
  dependsOn: UnitTests
  condition: eq(dependencies.UnitTests.result, 'Succeeded')

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_mi_e2e.py
Comment on lines +64 to +66
second = client.acquire_token_for_client(resource=_ARM_RESOURCE)
test.assertNotIn("error", second, "second acquisition failed: {}".format(second))
test.assertEqual(
Comment thread tests/test_mi_e2e.py
Comment on lines +41 to +46
_UAMI_CLIENT_ID = "6325cd32-9911-41f3-819c-416cdf9104e7"
_UAMI_OBJECT_ID = "ecb2ad92-3e30-4505-b79f-ac640d069f24"
_UAMI_RESOURCE_ID = (
"/subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourcegroups/"
"MSIV2-Testing-MSALNET/providers/Microsoft.ManagedIdentity/userAssignedIdentities/msiv2uami"
)
- stage: MIE2EImds
displayName: 'MI E2E - IMDS'
dependsOn: UnitTests
condition: eq(dependencies.UnitTests.result, 'Succeeded')
…p install

The self-hosted MI E2E pools carry the toolchain (like Go/.NET); pre-provision
msal's deps + pytest on them instead of installing at run time. This removes the
runtime "pip install", which the Azure Arc pool cannot do anyway - its egress TLS
inspection blocks files.pythonhosted.org (the same restriction MSAL Go documented).
Also drop --timeout so pytest-timeout is not required.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
Copilot AI review requested due to automatic review settings August 2, 2026 14:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (4)

.Pipelines/template-pipeline-stages.yml:343

  • The Azure Arc stage header still discusses "pip install" as if it were part of this stage, but the steps below require dependencies to be pre-provisioned and do not install anything. Rewording the comment will better reflect the actual constraint on the Arc pool.
#           Pool note: as observed for MSAL Go, the Arc machine's egress inspection can
#           reset TLS for package downloads. If "pip install" cannot reach PyPI, pre-provision
#           the dependencies on the pool (or use an offline wheel cache) and drop the install step.

tests/test_mi_e2e.py:71

  • requests.Session() is created but never closed. Even in short-lived E2E runs this can leave open connections/sockets and may trigger ResourceWarning on some Python versions. Close the session explicitly (try/finally) around the two token acquisitions.
    client = ManagedIdentityClient(managed_identity, http_client=requests.Session())

.Pipelines/template-pipeline-stages.yml:291

  • The stage header says the IMDS pool needs outbound access to PyPI for "pip install", but this stage intentionally does no runtime pip install (dependencies are described as pre-provisioned below). Updating the comment avoids confusing future maintainers when diagnosing pool requirements.

This issue also appears on line 341 of the same file.

#           Pool assumptions (self-hosted): Python 3.x on PATH and outbound access to
#           PyPI for "pip install". Adjust if the pool differs.

.Pipelines/template-pipeline-stages.yml:372

  • As written, this stage can succeed while running 0 effective MI tests (e.g., if Arc source detection fails and both test classes self-skip). Add a small pre-flight check that fails the job when the agent is not detected as Azure Arc, so pool misconfiguration is surfaced immediately.
          $ErrorActionPreference = 'Stop'
          python --version
          New-Item -ItemType Directory -Force -Path test-results | Out-Null
          python -m pytest -vv --junitxml=test-results/junit-mi-e2e-arc.xml tests/test_mi_e2e.py

…d on the agent PATH

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ad57850-7d1f-4b9a-bf9f-723d69a9687c
Copilot AI review requested due to automatic review settings August 2, 2026 14:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Suppressed comments (4)

.Pipelines/template-pipeline-stages.yml:356

  • Same as the IMDS stage: this stage targets a self-hosted pool (MISEAZUREARC) but does not currently skip forked PRs. Add a fork guard to avoid running untrusted code on self-hosted infrastructure.
- stage: MIE2EAzureArc
  displayName: 'MI E2E - Azure Arc'
  dependsOn: UnitTests
  condition: eq(dependencies.UnitTests.result, 'Succeeded')
  jobs:

.Pipelines/template-pipeline-stages.yml:382

  • Same issue as the IMDS stage: the script may pick up a non-Python-3 python.exe from PATH. Validate the major version and only proceed with Python 3.x.
          $py = (Get-Command python.exe -ErrorAction SilentlyContinue).Source
          if (-not $py) {
            $py = Get-ChildItem 'C:\Program Files\Python3*\python.exe','C:\Program Files (x86)\Python3*\python.exe','C:\Python3*\python.exe' -ErrorAction SilentlyContinue |
              Select-Object -First 1 -ExpandProperty FullName
          }
          if (-not $py) { throw 'Python not found on this agent. Install Python 3.x on the pool machine.' }

tests/test_mi_e2e.py:25

  • The suggested SHA-256 comparison relies on hashlib, but it isn't currently imported in this module.
import os
import unittest

import requests

.Pipelines/template-pipeline-stages.yml:323

  • The PowerShell script assumes python.exe on PATH is Python 3.x. If the agent has a Python 2 python.exe earlier on PATH, this will select it and the tests will fail in a non-obvious way.

Consider validating the major version (and falling back to C:\Program Files\Python3* search when it's not 3).

This issue also appears on line 377 of the same file.

          $py = (Get-Command python.exe -ErrorAction SilentlyContinue).Source
          if (-not $py) {
            $py = Get-ChildItem 'C:\Program Files\Python3*\python.exe','C:\Program Files (x86)\Python3*\python.exe','C:\Python3*\python.exe' -ErrorAction SilentlyContinue |
              Select-Object -First 1 -ExpandProperty FullName
          }
          if (-not $py) { throw 'Python not found on this agent. Install Python 3.x on the pool machine.' }

Comment thread tests/test_mi_e2e.py
Comment on lines +57 to +71
first = client.acquire_token_for_client(resource=_ARM_RESOURCE)
test.assertNotIn("error", first, "first acquisition failed: {}".format(first))
test.assertIn("access_token", first)
test.assertEqual(
"identity_provider", first.get("token_source"),
"first call should reach the identity provider")

second = client.acquire_token_for_client(resource=_ARM_RESOURCE)
test.assertNotIn("error", second, "second acquisition failed: {}".format(second))
test.assertEqual(
"cache", second.get("token_source"),
"second call should be served from the token cache")
test.assertEqual(
first["access_token"], second["access_token"],
"cached token should match the original token")
Comment on lines +293 to +297
- stage: MIE2EImds
displayName: 'MI E2E - IMDS'
dependsOn: UnitTests
condition: eq(dependencies.UnitTests.result, 'Succeeded')
jobs:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants