Skip to content

Commit

Permalink
[Identity] Update AzurePipelinesCredential (#35858)
Browse files Browse the repository at this point in the history
- 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 <paulvaneck@microsoft.com>
  • Loading branch information
pvaneck authored Jun 11, 2024
1 parent 47fdf5d commit 1552259
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 118 deletions.
6 changes: 5 additions & 1 deletion sdk/identity/azure-identity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
1 change: 1 addition & 0 deletions sdk/identity/azure-identity/TOKEN_CACHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
9 changes: 0 additions & 9 deletions sdk/identity/azure-identity/azure/identity/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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}."
)


Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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]:
Expand Down
2 changes: 1 addition & 1 deletion sdk/identity/azure-identity/azure/identity/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
VERSION = "1.17.0b3"
VERSION = "1.17.0"
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
# 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
from azure.core.credentials import AccessToken
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
Expand All @@ -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.
Expand All @@ -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,
Expand Down Expand Up @@ -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]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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="<tenant_id>",
client_id="<client_id>",
service_connection_id="<service_connection_id>",
Expand All @@ -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="<tenant_id>",
client_id="<client_id>",
service_connection_id="<service_connection_id>",
Expand Down
Loading

0 comments on commit 1552259

Please sign in to comment.