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] Testing service connections on pipeline #29451

Merged
merged 13 commits into from Apr 30, 2024
Expand Up @@ -14,16 +14,15 @@ import {
import { AzurePipelinesServiceConnectionCredentialOptions } from "./azurePipelinesServiceConnectionCredentialOptions";

const credentialName = "AzurePipelinesServiceConnectionCredential";
const OIDC_API_VERSION = "7.1";
const logger = credentialLogger(credentialName);
const OIDC_API_VERSION = "7.1-preview.1";

/**
* This credential is designed to be used in ADO Pipelines with service connections
* as a setup for workload identity federation.
*/
export class AzurePipelinesServiceConnectionCredential implements TokenCredential {
private clientAssertionCredential: ClientAssertionCredential | undefined;
private serviceConnectionId: string | undefined;

/**
* AzurePipelinesServiceConnectionCredential supports Federated Identity on Azure Pipelines through Service Connections.
Expand Down Expand Up @@ -51,7 +50,7 @@ export class AzurePipelinesServiceConnectionCredential implements TokenCredentia

if (clientId && tenantId && serviceConnectionId) {
this.ensurePipelinesSystemVars();
const oidcRequestUrl = `${process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${process.env.SYSTEM_TEAMPROJECTID}/_apis/distributedtask/hubs/build/plans/${process.env.SYSTEM_PLANID}/jobs/${process.env.SYSTEM_JOBID}/oidctoken?api-version=${OIDC_API_VERSION}&serviceConnectionId=${this.serviceConnectionId}`;
const oidcRequestUrl = `${process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${process.env.SYSTEM_TEAMPROJECTID}/_apis/distributedtask/hubs/build/plans/${process.env.SYSTEM_PLANID}/jobs/${process.env.SYSTEM_JOBID}/oidctoken?api-version=${OIDC_API_VERSION}&serviceConnectionId=${serviceConnectionId}`;
const systemAccessToken = `${process.env.SYSTEM_ACCESSTOKEN}`;
logger.info(
`Invoking ClientAssertionCredential with tenant ID: ${tenantId}, clientId: ${clientId} and service connection id: ${serviceConnectionId}`,
Expand All @@ -78,8 +77,7 @@ export class AzurePipelinesServiceConnectionCredential implements TokenCredentia
options?: GetTokenOptions,
): Promise<AccessToken> {
if (!this.clientAssertionCredential) {
const errorMessage = `${credentialName}: is unavailable. tenantId, clientId, and serviceConnectionId are required parameters.
To use Federation Identity in Azure Pipelines, these are required as inputs / env variables -
const errorMessage = `${credentialName}: is unavailable. To use Federation Identity in Azure Pipelines, these are required as input parameters / env variables -
tenantId,
clientId,
serviceConnectionId,
Expand Down Expand Up @@ -123,17 +121,27 @@ export class AzurePipelinesServiceConnectionCredential implements TokenCredentia
const response = await httpClient.sendRequest(request);
const text = response.bodyAsText;
if (!text) {
throw new AuthenticationError(
response.status,
`${credentialName}: Authenticated Failed. Received null token from OIDC request.`,
logger.error(
`${credentialName}: Authenticated Failed. Received null token from OIDC request. Response status- ${
response.status
}. Complete response - ${JSON.stringify(response)}`,
);
throw new CredentialUnavailableError(
`${credentialName}: Authenticated Failed. Received null token from OIDC request. Response status- ${
response.status
}. Complete response - ${JSON.stringify(response)}`,
);
}
const result = JSON.parse(text);
if (result?.oidcToken) {
return result.oidcToken;
} else {
throw new AuthenticationError(
response.status,
logger.error(
`${credentialName}: Authentication Failed. oidcToken field not detected in the response. Response = ${JSON.stringify(
result,
)}`,
);
throw new CredentialUnavailableError(
`${credentialName}: Authentication Failed. oidcToken field not detected in the response. Response = ${JSON.stringify(
result,
)}`,
Expand Down
Expand Up @@ -2,12 +2,13 @@
// Licensed under the MIT license.

import { AzurePipelinesServiceConnectionCredential } from "../../../src";
import { env, isLiveMode } from "@azure-tools/test-recorder";
import { isLiveMode } from "@azure-tools/test-recorder";
import { assert } from "@azure-tools/test-utils";

import { setLogLevel } from "@azure/logger";
setLogLevel("verbose");
KarishmaGhiya marked this conversation as resolved.
Show resolved Hide resolved
describe("AzurePipelinesServiceConnectionCredential", function () {
const scope = "https://vault.azure.net/.default";
const tenantId = env.IDENTITY_SP_TENANT_ID || env.AZURE_TENANT_ID!;
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when we record these tests, you'd want any env. calls to be made after the recorder is initialized (so like in the test body or a beforeEach block after creating the recorder. Just FYI. I could be wrong but I remember this being a problem for me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Though with this particular test, it's difficult to have recordings, since I cannot run locally :(

// const clientId = env.IDENTITY_SP_CLIENT_ID || env.AZURE_CLIENT_ID!;

it("authenticates with a valid service connection", async function () {
Expand Down