Skip to content

Commit f1bac74

Browse files
gladjohnCopilot
andcommitted
test(managed-identity): add real MI E2E tests + self-hosted CI stages
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
1 parent 422f9e5 commit f1bac74

2 files changed

Lines changed: 229 additions & 0 deletions

File tree

.Pipelines/template-pipeline-stages.yml

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ stages:
124124
--ignore=tests/test_e2e.py \
125125
--ignore=tests/test_e2e_manual.py \
126126
--ignore=tests/test_fmi_e2e.py \
127+
--ignore=tests/test_mi_e2e.py \
127128
--deselect tests/test_cryptography.py::CryptographyTestCase::test_ceiling_should_be_latest_cryptography_version_plus_three \
128129
--deselect tests/test_cryptography.py::CryptographyTestCase::test_should_be_run_with_latest_version_of_cryptography \
129130
2>&1 | tee test-results/pytest-unit.log
@@ -276,3 +277,121 @@ stages:
276277
- bash: rm -f "$(Agent.TempDirectory)/lab-auth.pfx"
277278
displayName: 'Remove lab certificate from agent'
278279
condition: always()
280+
281+
# ══════════════════════════════════════════════════════════════════════════════
282+
# Stage 4 - MI E2E (IMDS) - REAL managed identity token acquisition on an Azure VM.
283+
# Runs on the self-hosted "MISEManagedIdentity" pool, a Windows Azure VM
284+
# that has the lab system-assigned + user-assigned identities assigned.
285+
# Mirrors the MSAL Go "MI E2E - IMDS" stage. Only the IMDS cases in
286+
# tests/test_mi_e2e.py run here (gated on MSAL_TEST_MI_IMDS); the Arc case
287+
# self-skips. No lab certificate is needed - the VM's own managed identity
288+
# is used.
289+
#
290+
# Pool assumptions (self-hosted): Python 3.x on PATH and outbound access to
291+
# PyPI for "pip install". Adjust if the pool differs.
292+
# ══════════════════════════════════════════════════════════════════════════════
293+
- stage: MIE2EImds
294+
displayName: 'MI E2E - IMDS'
295+
dependsOn: UnitTests
296+
condition: eq(dependencies.UnitTests.result, 'Succeeded')
297+
jobs:
298+
- job: Pytest
299+
displayName: 'Managed Identity E2E - VM / IMDS'
300+
pool:
301+
type: windows
302+
isCustom: true
303+
name: MISEManagedIdentity
304+
timeoutInMinutes: 30
305+
variables:
306+
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
307+
steps:
308+
- task: PowerShell@2
309+
displayName: 'Install Python dependencies'
310+
inputs:
311+
targetType: 'inline'
312+
workingDirectory: '$(System.DefaultWorkingDirectory)'
313+
script: |
314+
python --version
315+
python -m pip install --upgrade pip
316+
python -m pip install -r requirements.txt
317+
python -m pip install pytest pytest-azurepipelines pytest-timeout
318+
319+
- task: PowerShell@2
320+
displayName: 'Run pytest (MI E2E - IMDS)'
321+
inputs:
322+
targetType: 'inline'
323+
workingDirectory: '$(System.DefaultWorkingDirectory)'
324+
script: |
325+
$ErrorActionPreference = 'Stop'
326+
New-Item -ItemType Directory -Force -Path test-results | Out-Null
327+
python -m pytest -vv --timeout=300 --junitxml=test-results/junit-mi-e2e-imds.xml tests/test_mi_e2e.py
328+
env:
329+
PYTHONUNBUFFERED: '1'
330+
MSAL_TEST_MI_IMDS: '1'
331+
332+
- task: PublishTestResults@2
333+
displayName: 'Publish MI E2E (IMDS) test results'
334+
condition: succeededOrFailed()
335+
inputs:
336+
testResultsFormat: 'JUnit'
337+
testResultsFiles: 'test-results/junit-mi-e2e-imds.xml'
338+
failTaskOnFailedTests: true
339+
testRunTitle: 'MI E2E - IMDS'
340+
341+
# ══════════════════════════════════════════════════════════════════════════════
342+
# Stage 5 - MI E2E (Azure Arc) - REAL managed identity token acquisition on an
343+
# Azure Arc-enabled machine. Runs on the self-hosted "MISEAZUREARC" pool.
344+
# Azure Arc supports the system-assigned identity only. Mirrors the MSAL Go
345+
# "MI E2E - Azure Arc" stage. tests/test_mi_e2e.py's Arc case runs here; the
346+
# IMDS cases self-skip (MSAL_TEST_MI_IMDS unset).
347+
#
348+
# Pool note: as observed for MSAL Go, the Arc machine's egress inspection can
349+
# reset TLS for package downloads. If "pip install" cannot reach PyPI, pre-provision
350+
# the dependencies on the pool (or use an offline wheel cache) and drop the install step.
351+
# ══════════════════════════════════════════════════════════════════════════════
352+
- stage: MIE2EAzureArc
353+
displayName: 'MI E2E - Azure Arc'
354+
dependsOn: UnitTests
355+
condition: eq(dependencies.UnitTests.result, 'Succeeded')
356+
jobs:
357+
- job: Pytest
358+
displayName: 'Managed Identity E2E - Azure Arc'
359+
pool:
360+
type: windows
361+
isCustom: true
362+
name: MISEAZUREARC
363+
timeoutInMinutes: 30
364+
variables:
365+
ob_outputDirectory: '$(Build.ArtifactStagingDirectory)'
366+
steps:
367+
- task: PowerShell@2
368+
displayName: 'Install Python dependencies'
369+
inputs:
370+
targetType: 'inline'
371+
workingDirectory: '$(System.DefaultWorkingDirectory)'
372+
script: |
373+
python --version
374+
python -m pip install --upgrade pip
375+
python -m pip install -r requirements.txt
376+
python -m pip install pytest pytest-azurepipelines pytest-timeout
377+
378+
- task: PowerShell@2
379+
displayName: 'Run pytest (MI E2E - Azure Arc)'
380+
inputs:
381+
targetType: 'inline'
382+
workingDirectory: '$(System.DefaultWorkingDirectory)'
383+
script: |
384+
$ErrorActionPreference = 'Stop'
385+
New-Item -ItemType Directory -Force -Path test-results | Out-Null
386+
python -m pytest -vv --timeout=300 --junitxml=test-results/junit-mi-e2e-arc.xml tests/test_mi_e2e.py
387+
env:
388+
PYTHONUNBUFFERED: '1'
389+
390+
- task: PublishTestResults@2
391+
displayName: 'Publish MI E2E (Azure Arc) test results'
392+
condition: succeededOrFailed()
393+
inputs:
394+
testResultsFormat: 'JUnit'
395+
testResultsFiles: 'test-results/junit-mi-e2e-arc.xml'
396+
failTaskOnFailedTests: true
397+
testRunTitle: 'MI E2E - Azure Arc'

tests/test_mi_e2e.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""End-to-end Managed Identity tests (real token acquisition).
2+
3+
These tests perform REAL token acquisition and therefore only run on the
4+
self-hosted Azure DevOps pools that are actual Azure VM / Azure Arc machines with
5+
the lab managed identities assigned:
6+
7+
* IMDS tests -> the "MISEManagedIdentity" pool (an Azure VM). Gated on the
8+
MSAL_TEST_MI_IMDS environment variable, which that pipeline
9+
stage sets. (DEFAULT_TO_VM is also the fallback source on hosted
10+
agents, so an explicit flag is used instead of source detection.)
11+
* Azure Arc -> the "MISEAZUREARC" pool (an Azure Arc-enabled machine). Gated on
12+
the Azure Arc source being detected on the machine.
13+
14+
They mirror the MSAL Go E2E tests
15+
(apps/tests/e2e/managedidentity_e2e_test.go and managedidentity_arc_e2e_test.go)
16+
and use the SAME lab identities and ARM resource, so both SDKs exercise the same
17+
lab configuration on the same machines.
18+
19+
Everywhere else (hosted agents, local dev) the tests self-skip.
20+
"""
21+
import os
22+
import unittest
23+
24+
import requests
25+
26+
from msal import (
27+
ManagedIdentityClient,
28+
SystemAssignedManagedIdentity,
29+
UserAssignedManagedIdentity,
30+
)
31+
from msal.managed_identity import get_managed_identity_source, AZURE_ARC
32+
33+
34+
# Azure Resource Manager resource. Matches the ARM scope used by the MSAL .NET and
35+
# Go managed identity E2E tests.
36+
_ARM_RESOURCE = "https://management.azure.com"
37+
38+
# User-assigned managed identities assigned to the MISEManagedIdentity VM. These are
39+
# the SAME values used by the MSAL Go / .NET IMDS E2E tests, so all SDKs exercise the
40+
# same lab configuration on the same VM.
41+
_UAMI_CLIENT_ID = "6325cd32-9911-41f3-819c-416cdf9104e7"
42+
_UAMI_OBJECT_ID = "ecb2ad92-3e30-4505-b79f-ac640d069f24"
43+
_UAMI_RESOURCE_ID = (
44+
"/subscriptions/c1686c51-b717-4fe0-9af3-24a20a41fb0c/resourcegroups/"
45+
"MSIV2-Testing-MSALNET/providers/Microsoft.ManagedIdentity/userAssignedIdentities/msiv2uami"
46+
)
47+
48+
49+
def _acquire_token_twice_assert_caching(test, managed_identity):
50+
"""Acquire an ARM token twice for the given managed identity and assert the first
51+
call reaches the identity provider while the second is served from the token cache.
52+
53+
Shared by the IMDS and Azure Arc E2E tests, mirroring the Go helper of the same name.
54+
"""
55+
client = ManagedIdentityClient(managed_identity, http_client=requests.Session())
56+
57+
first = client.acquire_token_for_client(resource=_ARM_RESOURCE)
58+
test.assertNotIn("error", first, "first acquisition failed: {}".format(first))
59+
test.assertIn("access_token", first)
60+
test.assertEqual(
61+
"identity_provider", first.get("token_source"),
62+
"first call should reach the identity provider")
63+
64+
second = client.acquire_token_for_client(resource=_ARM_RESOURCE)
65+
test.assertNotIn("error", second, "second acquisition failed: {}".format(second))
66+
test.assertEqual(
67+
"cache", second.get("token_source"),
68+
"second call should be served from the token cache")
69+
test.assertEqual(
70+
first["access_token"], second["access_token"],
71+
"cached token should match the original token")
72+
73+
74+
@unittest.skipUnless(
75+
os.getenv("MSAL_TEST_MI_IMDS"),
76+
"Set MSAL_TEST_MI_IMDS to run on the MISEManagedIdentity Azure VM (IMDS) pool")
77+
class ManagedIdentityImdsE2ETestCase(unittest.TestCase):
78+
"""Acquires ARM tokens over IMDS v1 for the system-assigned identity and each
79+
user-assigned identity binding (client id / resource id / object id). Each test
80+
asserts the first call reaches the identity provider and the second is cached."""
81+
82+
def test_system_assigned(self):
83+
_acquire_token_twice_assert_caching(self, SystemAssignedManagedIdentity())
84+
85+
def test_user_assigned_client_id(self):
86+
_acquire_token_twice_assert_caching(
87+
self, UserAssignedManagedIdentity(client_id=_UAMI_CLIENT_ID))
88+
89+
def test_user_assigned_resource_id(self):
90+
_acquire_token_twice_assert_caching(
91+
self, UserAssignedManagedIdentity(resource_id=_UAMI_RESOURCE_ID))
92+
93+
def test_user_assigned_object_id(self):
94+
_acquire_token_twice_assert_caching(
95+
self, UserAssignedManagedIdentity(object_id=_UAMI_OBJECT_ID))
96+
97+
98+
@unittest.skipUnless(
99+
get_managed_identity_source() == AZURE_ARC,
100+
"Runs only on an Azure Arc-enabled machine (the MISEAZUREARC pool)")
101+
class ManagedIdentityAzureArcE2ETestCase(unittest.TestCase):
102+
"""Azure Arc supports the system-assigned identity only, so unlike the IMDS tests
103+
there are no user-assigned variants."""
104+
105+
def test_system_assigned(self):
106+
_acquire_token_twice_assert_caching(self, SystemAssignedManagedIdentity())
107+
108+
109+
if __name__ == "__main__":
110+
unittest.main()

0 commit comments

Comments
 (0)