From 1552259af61e91653a862d29533ace4ed49fbab3 Mon Sep 17 00:00:00 2001 From: Paul Van Eck Date: Tue, 11 Jun 2024 16:03:58 -0700 Subject: [PATCH] [Identity] Update AzurePipelinesCredential (#35858) - OIDC API version updated to 7.1. - Use the `SYSTEM_OIDCREQUESTURI` envvar for the base OIDC url - Added a new required parameter: "system_access_token". - Bump version to 1.17.0. Signed-off-by: Paul Van Eck --- sdk/identity/azure-identity/CHANGELOG.md | 6 +- sdk/identity/azure-identity/TOKEN_CACHING.md | 1 + .../azure/identity/_constants.py | 9 --- .../identity/_credentials/azure_pipelines.py | 79 +++++++++---------- .../azure-identity/azure/identity/_version.py | 2 +- .../aio/_credentials/azure_pipelines.py | 34 +++++--- .../credential_creation_code_snippets.py | 4 + .../tests/test_azure_pipelines_credential.py | 63 ++++++++------- .../test_azure_pipelines_credential_async.py | 44 +++++++---- sdk/identity/test-resources-post.ps1 | 3 - sdk/identity/test-resources-pre.ps1 | 3 +- sdk/identity/tests.yml | 18 +++++ 12 files changed, 148 insertions(+), 118 deletions(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index ef0ab1ab26f4..96f5fb1f12ae 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,11 +1,15 @@ # Release History -## 1.17.0b3 (Unreleased) +## 1.17.0 (Unreleased) ### Features Added ### Breaking Changes +> These changes do not impact the API of stable versions such as 1.16.0. +> Only code written against a beta version such as 1.17.0b1 is affected. +- `AzurePipelinesCredential` now has a required keyword argument `system_access_token`. ([#35858](https://github.com/Azure/azure-sdk-for-python/pull/35858)) + ### Bugs Fixed - Allow credential chains to continue when an IMDS probe request returns a non-JSON response in `ManagedIdentityCredential`. ([#36016](https://github.com/Azure/azure-sdk-for-python/pull/36016)) diff --git a/sdk/identity/azure-identity/TOKEN_CACHING.md b/sdk/identity/azure-identity/TOKEN_CACHING.md index 66850c5b4077..e7691bc2fad6 100644 --- a/sdk/identity/azure-identity/TOKEN_CACHING.md +++ b/sdk/identity/azure-identity/TOKEN_CACHING.md @@ -90,6 +90,7 @@ The following table indicates the state of in-memory and persistent caching in e | `AuthorizationCodeCredential` | Supported | Supported | | `AzureCliCredential` | Not Supported | Not Supported | | `AzureDeveloperCliCredential` | Not Supported | Not Supported | +| `AzurePipelinesCredential` | Supported | Not Supported | | `AzurePowershellCredential` | Not Supported | Not Supported | | `ClientAssertionCredential` | Supported | Not Supported | | `CertificateCredential` | Supported | Supported | diff --git a/sdk/identity/azure-identity/azure/identity/_constants.py b/sdk/identity/azure-identity/azure/identity/_constants.py index e41a80b03c91..a272179ceed6 100644 --- a/sdk/identity/azure-identity/azure/identity/_constants.py +++ b/sdk/identity/azure-identity/azure/identity/_constants.py @@ -2,7 +2,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -# cspell:ignore teamprojectid, planid, jobid, oidctoken DEVELOPER_SIGN_ON_CLIENT_ID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" AZURE_VSCODE_CLIENT_ID = "aebc6443-996d-45c2-90f0-388ff96faa56" @@ -54,11 +53,3 @@ class EnvironmentVariables: AZURE_FEDERATED_TOKEN_FILE = "AZURE_FEDERATED_TOKEN_FILE" WORKLOAD_IDENTITY_VARS = (AZURE_AUTHORITY_HOST, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE) - - # Azure Pipelines specific environment variables - SYSTEM_TEAMFOUNDATIONCOLLECTIONURI = "SYSTEM_TEAMFOUNDATIONCOLLECTIONURI" - SYSTEM_TEAMPROJECTID = "SYSTEM_TEAMPROJECTID" - SYSTEM_PLANID = "SYSTEM_PLANID" - SYSTEM_JOBID = "SYSTEM_JOBID" - SYSTEM_ACCESSTOKEN = "SYSTEM_ACCESSTOKEN" - SYSTEM_HOSTTYPE = "SYSTEM_HOSTTYPE" diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/azure_pipelines.py b/sdk/identity/azure-identity/azure/identity/_credentials/azure_pipelines.py index bf4ed32e2683..ea074b406cc7 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/azure_pipelines.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/azure_pipelines.py @@ -2,7 +2,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -# cspell:ignore teamprojectid, planid, jobid, oidctoken +# cspell:ignore oidcrequesturi import os from typing import Any, Optional @@ -14,42 +14,27 @@ from .. import CredentialUnavailableError from .._internal import validate_tenant_id from .._internal.pipeline import build_pipeline -from .._constants import EnvironmentVariables as ev - - -AZURE_PIPELINES_VARS = ( - ev.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI, - ev.SYSTEM_TEAMPROJECTID, - ev.SYSTEM_PLANID, - ev.SYSTEM_JOBID, - ev.SYSTEM_ACCESSTOKEN, - ev.SYSTEM_HOSTTYPE, -) -OIDC_API_VERSION = "7.1-preview.1" - - -def build_oidc_request(service_connection_id: str) -> HttpRequest: - base_uri = os.environ[ev.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI].rstrip("/") - url = ( - f"{base_uri}/{os.environ[ev.SYSTEM_TEAMPROJECTID]}/_apis/distributedtask/hubs/" - f"{os.environ[ev.SYSTEM_HOSTTYPE]}/plans/{os.environ[ev.SYSTEM_PLANID]}/jobs/{os.environ[ev.SYSTEM_JOBID]}/" - f"oidctoken?api-version={OIDC_API_VERSION}&serviceConnectionId={service_connection_id}" - ) - access_token = os.environ[ev.SYSTEM_ACCESSTOKEN] + + +SYSTEM_OIDCREQUESTURI = "SYSTEM_OIDCREQUESTURI" +OIDC_API_VERSION = "7.1" +TROUBLESHOOTING_GUIDE = "https://aka.ms/azsdk/python/identity/azurepipelinescredential/troubleshoot" + + +def build_oidc_request(service_connection_id: str, access_token: str) -> HttpRequest: + base_uri = os.environ[SYSTEM_OIDCREQUESTURI].rstrip("/") + url = f"{base_uri}?api-version={OIDC_API_VERSION}&serviceConnectionId={service_connection_id}" headers = {"Content-Type": "application/json", "Authorization": f"Bearer {access_token}"} return HttpRequest("POST", url, headers=headers) def validate_env_vars(): - missing_vars = [] - for var in AZURE_PIPELINES_VARS: - if var not in os.environ or not os.environ[var]: - missing_vars.append(var) - if missing_vars: + if SYSTEM_OIDCREQUESTURI not in os.environ: raise CredentialUnavailableError( - message=f"Missing values for environment variables: {', '.join(missing_vars)}. " - f"AzurePipelinesCredential is intended for use in Azure Pipelines where the following environment " - f"variables are set: {AZURE_PIPELINES_VARS}." + message=f"Missing value for the {SYSTEM_OIDCREQUESTURI} environment variable. " + f"AzurePipelinesCredential is intended for use in Azure Pipelines where the " + f"{SYSTEM_OIDCREQUESTURI} environment variable is set. Please refer to the " + f"troubleshooting guide at {TROUBLESHOOTING_GUIDE}." ) @@ -59,10 +44,17 @@ class AzurePipelinesCredential: This credential enables authentication in Azure Pipelines using workload identity federation for Azure service connections. - :keyword str service_connection_id: The service connection ID, as found in the querystring's resourceId key. - Required. - :keyword str tenant_id: ID of the application's Microsoft Entra tenant. Also called its "directory" ID. - :keyword str client_id: The client ID of a Microsoft Entra app registration. + :keyword str tenant_id: The tenant ID for the service connection. Required. + :keyword str client_id: The client ID for the service connection. Required. + :keyword str service_connection_id: The service connection ID for the service connection associated with the + pipeline. From the service connection's configuration page URL in the Azure DevOps web portal, the ID + is the value of the "resourceId" query parameter. Required. + :keyword str system_access_token: The pipeline's System.AccessToken value. It is recommended to assign the value + of System.AccessToken to a secure variable in the Azure Pipelines environment. See + https://learn.microsoft.com/azure/devops/pipelines/build/variables#systemaccesstoken for more info. Required. + :keyword str authority: Authority of a Microsoft Entra endpoint, for example "login.microsoftonline.com", + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` + defines authorities for other clouds. :keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id" for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the application can access. @@ -83,19 +75,22 @@ def __init__( tenant_id: str, client_id: str, service_connection_id: str, + system_access_token: str, **kwargs: Any, ) -> None: - if not tenant_id or not client_id or not service_connection_id: - raise ValueError("tenant_id, client_id, and service_connection_id are required.") + if not system_access_token or not tenant_id or not client_id or not service_connection_id: + raise ValueError( + "'tenant_id', 'client_id', 'service_connection_id', and 'system_access_token' must be passed in as " + f"keyword arguments. Please refer to the troubleshooting guide at {TROUBLESHOOTING_GUIDE}." + ) validate_tenant_id(tenant_id) - + self._system_access_token = system_access_token self._service_connection_id = service_connection_id self._client_assertion_credential = ClientAssertionCredential( tenant_id=tenant_id, client_id=client_id, func=self._get_oidc_token, **kwargs ) self._pipeline = build_pipeline(**kwargs) - self._env_validated = False def get_token( self, @@ -125,15 +120,13 @@ def get_token( :raises ~azure.core.exceptions.ClientAuthenticationError: authentication failed. The error's ``message`` attribute gives a reason. """ - if not self._env_validated: - validate_env_vars() - self._env_validated = True + validate_env_vars() return self._client_assertion_credential.get_token( *scopes, claims=claims, tenant_id=tenant_id, enable_cae=enable_cae, **kwargs ) def _get_oidc_token(self) -> str: - request = build_oidc_request(self._service_connection_id) + request = build_oidc_request(self._service_connection_id, self._system_access_token) response = self._pipeline.run(request, retry_on_methods=[request.method]) http_response: HttpResponse = response.http_response if http_response.status_code not in [200]: diff --git a/sdk/identity/azure-identity/azure/identity/_version.py b/sdk/identity/azure-identity/azure/identity/_version.py index af5add1f87cb..0276cdca916b 100644 --- a/sdk/identity/azure-identity/azure/identity/_version.py +++ b/sdk/identity/azure-identity/azure/identity/_version.py @@ -2,4 +2,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -VERSION = "1.17.0b3" +VERSION = "1.17.0" diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_pipelines.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_pipelines.py index 091d7e982523..918cae86a921 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_pipelines.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/azure_pipelines.py @@ -2,7 +2,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -# cspell:ignore teamprojectid, planid, jobid, oidctoken from typing import Any, Optional from azure.core.exceptions import ClientAuthenticationError @@ -10,7 +9,7 @@ from azure.core.rest import HttpResponse from .client_assertion import ClientAssertionCredential -from ..._credentials.azure_pipelines import build_oidc_request, validate_env_vars +from ..._credentials.azure_pipelines import TROUBLESHOOTING_GUIDE, build_oidc_request, validate_env_vars from .._internal import AsyncContextManager from ..._internal import validate_tenant_id from ..._internal.pipeline import build_pipeline @@ -22,10 +21,17 @@ class AzurePipelinesCredential(AsyncContextManager): This credential enables authentication in Azure Pipelines using workload identity federation for Azure service connections. - :keyword str service_connection_id: The service connection ID, as found in the querystring's resourceId key. - Required. - :keyword str tenant_id: ID of the application's Microsoft Entra tenant. Also called its "directory" ID. - :keyword str client_id: The client ID of a Microsoft Entra app registration. + :keyword str tenant_id: The tenant ID for the service connection. Required. + :keyword str client_id: The client ID for the service connection. Required. + :keyword str service_connection_id: The service connection ID for the service connection associated with the + pipeline. From the service connection's configuration page URL in the Azure DevOps web portal, the ID + is the value of the "resourceId" query parameter. Required. + :keyword str system_access_token: The pipeline's System.AccessToken value. It is recommended to assign the value + of System.AccessToken to a secure variable in the Azure Pipelines environment. See + https://learn.microsoft.com/azure/devops/pipelines/build/variables#systemaccesstoken for more info. Required. + :keyword str authority: Authority of a Microsoft Entra endpoint, for example "login.microsoftonline.com", + the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` + defines authorities for other clouds. :keyword List[str] additionally_allowed_tenants: Specifies tenants in addition to the specified "tenant_id" for which the credential may acquire tokens. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the application can access. @@ -46,19 +52,23 @@ def __init__( tenant_id: str, client_id: str, service_connection_id: str, + system_access_token: str, **kwargs: Any, ) -> None: - if not tenant_id or not client_id or not service_connection_id: - raise ValueError("tenant_id, client_id, and service_connection_id are required.") + if not system_access_token or not tenant_id or not client_id or not service_connection_id: + raise ValueError( + "'tenant_id', 'client_id','service_connection_id', and 'system_access_token' must be passed in as " + f"keyword arguments. Please refer to the troubleshooting guide at {TROUBLESHOOTING_GUIDE}." + ) validate_tenant_id(tenant_id) + self._system_access_token = system_access_token self._service_connection_id = service_connection_id self._client_assertion_credential = ClientAssertionCredential( tenant_id=tenant_id, client_id=client_id, func=self._get_oidc_token, **kwargs ) self._pipeline = build_pipeline(**kwargs) - self._env_validated = False async def get_token( self, @@ -88,15 +98,13 @@ async def get_token( :raises ~azure.core.exceptions.ClientAuthenticationError: authentication failed. The error's ``message`` attribute gives a reason. """ - if not self._env_validated: - validate_env_vars() - self._env_validated = True + validate_env_vars() return await self._client_assertion_credential.get_token( *scopes, claims=claims, tenant_id=tenant_id, enable_cae=enable_cae, **kwargs ) def _get_oidc_token(self) -> str: - request = build_oidc_request(self._service_connection_id) + request = build_oidc_request(self._service_connection_id, self._system_access_token) response = self._pipeline.run(request, retry_on_methods=[request.method]) http_response: HttpResponse = response.http_response if http_response.status_code not in [200]: diff --git a/sdk/identity/azure-identity/samples/credential_creation_code_snippets.py b/sdk/identity/azure-identity/samples/credential_creation_code_snippets.py index ba1ae7aa2b05..5e199302963b 100644 --- a/sdk/identity/azure-identity/samples/credential_creation_code_snippets.py +++ b/sdk/identity/azure-identity/samples/credential_creation_code_snippets.py @@ -353,9 +353,11 @@ async def create_workload_identity_credential_async(): def create_azure_pipelines_credential(): # [START create_azure_pipelines_credential] + import os from azure.identity import AzurePipelinesCredential credential = AzurePipelinesCredential( + system_access_token=os.environ["SYSTEM_ACCESSTOKEN"], tenant_id="", client_id="", service_connection_id="", @@ -365,9 +367,11 @@ def create_azure_pipelines_credential(): async def create_azure_pipelines_credential_async(): # [START create_azure_pipelines_credential_async] + import os from azure.identity.aio import AzurePipelinesCredential credential = AzurePipelinesCredential( + system_access_token=os.environ["SYSTEM_ACCESSTOKEN"], tenant_id="", client_id="", service_connection_id="", diff --git a/sdk/identity/azure-identity/tests/test_azure_pipelines_credential.py b/sdk/identity/azure-identity/tests/test_azure_pipelines_credential.py index 4454d30d0fa4..459f2e2251f6 100644 --- a/sdk/identity/azure-identity/tests/test_azure_pipelines_credential.py +++ b/sdk/identity/azure-identity/tests/test_azure_pipelines_credential.py @@ -2,7 +2,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -# cspell:ignore teamprojectid, planid, jobid, oidctoken +# cspell:ignore oidcrequesturi import os from unittest.mock import MagicMock, patch @@ -14,29 +14,43 @@ ClientAssertionCredential, CredentialUnavailableError, ) -from azure.identity._constants import EnvironmentVariables -from azure.identity._credentials.azure_pipelines import build_oidc_request, OIDC_API_VERSION +from azure.identity._credentials.azure_pipelines import SYSTEM_OIDCREQUESTURI, OIDC_API_VERSION, build_oidc_request def test_azure_pipelines_credential_initialize(): + system_access_token = "token" service_connection_id = "connection-id" tenant_id = "tenant-id" client_id = "client-id" credential = AzurePipelinesCredential( + system_access_token=system_access_token, tenant_id=tenant_id, client_id=client_id, service_connection_id=service_connection_id, ) assert credential._service_connection_id == service_connection_id + assert credential._system_access_token == system_access_token assert isinstance(credential._client_assertion_credential, ClientAssertionCredential) +def test_azure_pipelines_credential_initialize_empty_kwarg(): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(ValueError): + AzurePipelinesCredential( + system_access_token="token", client_id="client-id", tenant_id="tenant-id", service_connection_id="" + ) + + def test_azure_pipelines_credential_context_manager(): transport = MagicMock() credential = AzurePipelinesCredential( - client_id="client-id", tenant_id="tenant-id", service_connection_id="connection-id", transport=transport + system_access_token="token", + client_id="client-id", + tenant_id="tenant-id", + service_connection_id="connection-id", + transport=transport, ) with credential: @@ -48,52 +62,32 @@ def test_azure_pipelines_credential_context_manager(): def test_build_oidc_request(): service_connection_id = "connection-id" collection_uri = "https://example.com" - project_id = "team-project-id" - plan_id = "plan-id" - job_id = "job-id" access_token = "access-token" - host_type = "build" - environment = { - EnvironmentVariables.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: collection_uri, - EnvironmentVariables.SYSTEM_TEAMPROJECTID: project_id, - EnvironmentVariables.SYSTEM_PLANID: plan_id, - EnvironmentVariables.SYSTEM_JOBID: job_id, - EnvironmentVariables.SYSTEM_ACCESSTOKEN: access_token, - EnvironmentVariables.SYSTEM_HOSTTYPE: host_type, - } + environment = {SYSTEM_OIDCREQUESTURI: collection_uri} with patch.dict("os.environ", environment, clear=True): - request: HttpRequest = build_oidc_request(service_connection_id) + request: HttpRequest = build_oidc_request(service_connection_id, access_token) assert request.method == "POST" - assert request.url.startswith( - f"{collection_uri}/{project_id}/_apis/distributedtask/hubs/{host_type}/plans/{plan_id}/" - f"jobs/{job_id}/oidctoken" - ) + assert request.url.startswith(collection_uri) assert f"api-version={OIDC_API_VERSION}" in request.url assert f"serviceConnectionId={service_connection_id}" in request.url assert request.headers["Content-Type"] == "application/json" assert request.headers["Authorization"] == f"Bearer {access_token}" -def test_azure_pipelines_credential_missing_env_var(): +def test_azure_pipelines_credential_missing_system_env_var(): credential = AzurePipelinesCredential( + system_access_token="token", client_id="client-id", tenant_id="tenant-id", service_connection_id="connection-id", ) - environment = { - EnvironmentVariables.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: "foo", - EnvironmentVariables.SYSTEM_TEAMPROJECTID: "foo", - EnvironmentVariables.SYSTEM_PLANID: "foo", - EnvironmentVariables.SYSTEM_JOBID: "foo", - EnvironmentVariables.SYSTEM_HOSTTYPE: "foo", - } - with patch.dict("os.environ", environment, clear=True): + with patch.dict("os.environ", {}, clear=True): with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") - assert f"Missing values for environment variables: {EnvironmentVariables.SYSTEM_ACCESSTOKEN}" in str(ex.value) + assert f"Missing value for the {SYSTEM_OIDCREQUESTURI} environment variable" in str(ex.value) def test_azure_pipelines_credential_in_chain(): @@ -102,7 +96,10 @@ def test_azure_pipelines_credential_in_chain(): with patch.dict("os.environ", {}, clear=True): chain_credential = ChainedTokenCredential( AzurePipelinesCredential( - tenant_id="tenant-id", client_id="client-id", service_connection_id="connection-id" + system_access_token="token", + tenant_id="tenant-id", + client_id="client-id", + service_connection_id="connection-id", ), mock_credential, ) @@ -112,6 +109,7 @@ def test_azure_pipelines_credential_in_chain(): @pytest.mark.live_test_only("Requires Azure Pipelines environment with configured service connection") def test_azure_pipelines_credential_authentication(): + system_access_token = os.environ.get("SYSTEM_ACCESSTOKEN", "") service_connection_id = os.environ.get("AZURE_SERVICE_CONNECTION_ID", "") tenant_id = os.environ.get("AZURE_SERVICE_CONNECTION_TENANT_ID", "") client_id = os.environ.get("AZURE_SERVICE_CONNECTION_CLIENT_ID", "") @@ -122,6 +120,7 @@ def test_azure_pipelines_credential_authentication(): pytest.skip("This test requires environment variables to be set") credential = AzurePipelinesCredential( + system_access_token=system_access_token, tenant_id=tenant_id, client_id=client_id, service_connection_id=service_connection_id, diff --git a/sdk/identity/azure-identity/tests/test_azure_pipelines_credential_async.py b/sdk/identity/azure-identity/tests/test_azure_pipelines_credential_async.py index e2510624fac9..cc3cf313a88b 100644 --- a/sdk/identity/azure-identity/tests/test_azure_pipelines_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_azure_pipelines_credential_async.py @@ -2,36 +2,52 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -# cspell:ignore teamprojectid, planid, jobid, oidctoken +# cspell:ignore oidcrequesturi import os from unittest.mock import AsyncMock, patch import pytest from azure.identity import CredentialUnavailableError +from azure.identity._credentials.azure_pipelines import SYSTEM_OIDCREQUESTURI from azure.identity.aio import AzurePipelinesCredential, ChainedTokenCredential, ClientAssertionCredential -from azure.identity._constants import EnvironmentVariables def test_azure_pipelines_credential_initialize(): + system_access_token = "token" service_connection_id = "connection-id" tenant_id = "tenant-id" client_id = "client-id" credential = AzurePipelinesCredential( + system_access_token=system_access_token, tenant_id=tenant_id, client_id=client_id, service_connection_id=service_connection_id, ) assert credential._service_connection_id == service_connection_id + assert credential._system_access_token == system_access_token assert isinstance(credential._client_assertion_credential, ClientAssertionCredential) +@pytest.mark.asyncio +async def test_azure_pipelines_credential_initialize_empty_kwarg(): + with patch.dict("os.environ", {}, clear=True): + with pytest.raises(ValueError): + AzurePipelinesCredential( + system_access_token="token", client_id="client-id", tenant_id="tenant-id", service_connection_id="" + ) + + @pytest.mark.asyncio async def test_azure_pipelines_credential_context_manager(): transport = AsyncMock() credential = AzurePipelinesCredential( - client_id="client-id", tenant_id="tenant-id", service_connection_id="connection-id", transport=transport + system_access_token="token", + client_id="client-id", + tenant_id="tenant-id", + service_connection_id="connection-id", + transport=transport, ) async with credential: @@ -41,23 +57,18 @@ async def test_azure_pipelines_credential_context_manager(): @pytest.mark.asyncio -async def test_azure_pipelines_credential_missing_env_var(): +async def test_azure_pipelines_credential_missing_system_env_var(): credential = AzurePipelinesCredential( + system_access_token="token", client_id="client-id", tenant_id="tenant-id", service_connection_id="connection-id", ) - environment = { - EnvironmentVariables.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI: "foo", - EnvironmentVariables.SYSTEM_TEAMPROJECTID: "foo", - EnvironmentVariables.SYSTEM_PLANID: "foo", - EnvironmentVariables.SYSTEM_JOBID: "foo", - } - - with patch.dict("os.environ", environment, clear=True): + + with patch.dict("os.environ", {}, clear=True): with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") - assert f"Missing values for environment variables: {EnvironmentVariables.SYSTEM_ACCESSTOKEN}" in str(ex.value) + assert f"Missing value for the {SYSTEM_OIDCREQUESTURI} environment variable" in str(ex.value) @pytest.mark.asyncio @@ -67,7 +78,10 @@ async def test_azure_pipelines_credential_in_chain(): with patch.dict("os.environ", {}, clear=True): chain_credential = ChainedTokenCredential( AzurePipelinesCredential( - tenant_id="tenant-id", client_id="client-id", service_connection_id="connection-id" + system_access_token="token", + tenant_id="tenant-id", + client_id="client-id", + service_connection_id="connection-id", ), mock_credential, ) @@ -78,6 +92,7 @@ async def test_azure_pipelines_credential_in_chain(): @pytest.mark.asyncio @pytest.mark.live_test_only("Requires Azure Pipelines environment with configured service connection") async def test_azure_pipelines_credential_authentication(): + system_access_token = os.environ.get("SYSTEM_ACCESSTOKEN", "") service_connection_id = os.environ.get("AZURE_SERVICE_CONNECTION_ID", "") tenant_id = os.environ.get("AZURE_SERVICE_CONNECTION_TENANT_ID", "") client_id = os.environ.get("AZURE_SERVICE_CONNECTION_CLIENT_ID", "") @@ -88,6 +103,7 @@ async def test_azure_pipelines_credential_authentication(): pytest.skip("This test requires environment variables to be set") credential = AzurePipelinesCredential( + system_access_token=system_access_token, tenant_id=tenant_id, client_id=client_id, service_connection_id=service_connection_id, diff --git a/sdk/identity/test-resources-post.ps1 b/sdk/identity/test-resources-post.ps1 index 941efb1a21fe..9fe48c32b791 100644 --- a/sdk/identity/test-resources-post.ps1 +++ b/sdk/identity/test-resources-post.ps1 @@ -29,9 +29,6 @@ $workingFolder = $webappRoot; Write-Host "Working directory: $workingFolder" -az login --service-principal -u $DeploymentOutputs['IDENTITY_CLIENT_ID'] -p $DeploymentOutputs['IDENTITY_CLIENT_SECRET'] --tenant $DeploymentOutputs['IDENTITY_TENANT_ID'] -az account set --subscription $DeploymentOutputs['IDENTITY_SUBSCRIPTION_ID'] - Write-Host "Sleeping for a bit to ensure container registry is ready." Start-Sleep -s 60 diff --git a/sdk/identity/test-resources-pre.ps1 b/sdk/identity/test-resources-pre.ps1 index c943e032d29b..66fe9e09b85f 100644 --- a/sdk/identity/test-resources-pre.ps1 +++ b/sdk/identity/test-resources-pre.ps1 @@ -53,8 +53,7 @@ pip install azure-cli=="2.56.0" $az_version = az version Write-Host "Azure CLI version: $az_version" -az login --service-principal -u $TestApplicationId -p $TestApplicationSecret --tenant $TenantId -az account set --subscription $SubscriptionId +az login --service-principal -u $TestApplicationId --tenant $TenantId --allow-no-subscriptions --federated-token $env:ARM_OIDC_TOKEN $versions = az aks get-versions -l westus -o json | ConvertFrom-Json Write-Host "AKS versions: $($versions | ConvertTo-Json -Depth 100)" $patchVersions = $versions.values | Where-Object { $_.isPreview -eq $null } | Select-Object -ExpandProperty patchVersions diff --git a/sdk/identity/tests.yml b/sdk/identity/tests.yml index 60bec1921122..9e5c93a65eaa 100644 --- a/sdk/identity/tests.yml +++ b/sdk/identity/tests.yml @@ -1,8 +1,24 @@ +# cSpell:ignore pscore +# cSpell:ignore issecret trigger: none extends: template: ../../eng/pipelines/templates/stages/archetype-sdk-tests.yml parameters: + PreSteps: + - task: AzureCLI@2 + displayName: Set OIDC variables + env: + ARM_OIDC_TOKEN: $(ARM_OIDC_TOKEN) + inputs: + azureSubscription: azure-sdk-tests + scriptType: pscore + scriptLocation: inlineScript + addSpnToEnvironment: true + inlineScript: | + Write-Host "##vso[task.setvariable variable=ARM_CLIENT_ID;issecret=true]$($env:servicePrincipalId)" + Write-Host "##vso[task.setvariable variable=ARM_TENANT_ID;issecret=true]$($env:tenantId)" + Write-Host "##vso[task.setvariable variable=ARM_OIDC_TOKEN;issecret=true]$($env:idToken)" ServiceDirectory: identity EnvVars: AZURE_CLIENT_ID: $(IDENTITY_SP_CLIENT_ID) @@ -17,6 +33,8 @@ extends: SubscriptionConfigurations: - $(sub-config-azure-cloud-test-resources) - $(sub-config-identity-test-resources) + ServiceConnection: azure-sdk-tests + UseFederatedAuth: true ${{ if contains(variables['Build.DefinitionName'], 'tests-weekly') }}: # Test Managed Identity integrations tests on weekly tests pipeline. AdditionalMatrixConfigs: