Skip to content

Commit

Permalink
[Identity] Update AzurePipelinesCredential
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".

Signed-off-by: Paul Van Eck <[email protected]>
  • Loading branch information
pvaneck committed Jun 5, 2024
1 parent edccdfa commit 99165c4
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 116 deletions.
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,15 @@ 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 system_access_token: The pipeline's System.AccessToken value. It is recommended to assign the value
of System.AccessToke to a secure variable in the Azure Pipelines environment. Required.
: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. 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 @@ -80,22 +70,25 @@ class AzurePipelinesCredential:
def __init__(
self,
*,
system_access_token: str,
tenant_id: str,
client_id: str,
service_connection_id: 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(
"'system_access_token', 'tenant_id', 'client_id', and 'service_connection_id' 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 +118,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
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,15 @@ 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 system_access_token: The pipeline's System.AccessToken value. It is recommended to assign the value
of System.AccessToke to a secure variable in the Azure Pipelines environment. Required.
: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. 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 @@ -43,22 +47,26 @@ class AzurePipelinesCredential(AsyncContextManager):
def __init__(
self,
*,
system_access_token: str,
tenant_id: str,
client_id: str,
service_connection_id: 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(
"'system_access_token', 'tenant_id', 'client_id', and 'service_connection_id' 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 +96,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 99165c4

Please sign in to comment.