Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Identity] Update AzurePipelinesCredential #35858

Merged
merged 1 commit into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

### Other Changes
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
pvaneck marked this conversation as resolved.
Show resolved Hide resolved
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,
xiangyan99 marked this conversation as resolved.
Show resolved Hide resolved
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
pvaneck marked this conversation as resolved.
Show resolved Hide resolved
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