From f0a00378912f27f30509fe8230abcfdf5b58a7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moreno?= Date: Thu, 7 Nov 2024 06:43:11 +0100 Subject: [PATCH] use longer lived access tokens for cosmosdb auth (#233255) --- build/azure-pipelines/common/publish.js | 39 ++++++++++++++++++---- build/azure-pipelines/common/publish.ts | 44 +++++++++++++++++++++---- build/azure-pipelines/product-build.yml | 2 +- 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js index a9e7d60257736..5b7acc2000158 100644 --- a/build/azure-pipelines/common/publish.js +++ b/build/azure-pipelines/common/publish.js @@ -4,6 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); +exports.getAccessToken = getAccessToken; const fs = require("fs"); const path = require("path"); const stream_1 = require("stream"); @@ -41,6 +42,32 @@ class Temp { } } } +/** + * Gets an access token converted from a WIF/OIDC id token. + * We need this since this build job takes a while to run and while id tokens live for 10 minutes only, access tokens live for 24 hours. + * Source: https://goodworkaround.com/2021/12/21/another-deep-dive-into-azure-ad-workload-identity-federation-using-github-actions/ + */ +async function getAccessToken(endpoint, tenantId, clientId, idToken) { + const body = new URLSearchParams({ + scope: `${endpoint}.default`, + client_id: clientId, + grant_type: 'client_credentials', + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: encodeURIComponent(idToken) + }); + const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: body.toString() + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const aadToken = await response.json(); + return aadToken.access_token; +} function isCreateProvisionedFilesErrorResponse(response) { return response?.ErrorDetails?.Code !== undefined; } @@ -473,7 +500,7 @@ function getRealType(type) { return type; } } -async function processArtifact(artifact, artifactFilePath) { +async function processArtifact(artifact, artifactFilePath, cosmosDBAccessToken) { const log = (...args) => console.log(`[${artifact.name}]`, ...args); const match = /^vscode_(?[^_]+)_(?[^_]+)(?:_legacy)?_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); if (!match) { @@ -494,8 +521,7 @@ async function processArtifact(artifact, artifactFilePath) { log('Creating asset...', JSON.stringify(asset, undefined, 2)); await (0, retry_1.retry)(async (attempt) => { log(`Creating asset in Cosmos DB (attempt ${attempt})...`); - const aadCredentials = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); - const client = new cosmos_1.CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), aadCredentials }); + const client = new cosmos_1.CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken}`) }); const scripts = client.database('builds').container(quality).scripts; await scripts.storedProcedure('createAsset').execute('', [commit, asset, true]); }); @@ -509,8 +535,8 @@ async function processArtifact(artifact, artifactFilePath) { // the CDN and finally update the build in Cosmos DB. async function main() { if (!node_worker_threads_1.isMainThread) { - const { artifact, artifactFilePath } = node_worker_threads_1.workerData; - await processArtifact(artifact, artifactFilePath); + const { artifact, artifactFilePath, cosmosDBAccessToken } = node_worker_threads_1.workerData; + await processArtifact(artifact, artifactFilePath, cosmosDBAccessToken); return; } const done = new State(); @@ -539,6 +565,7 @@ async function main() { } let resultPromise = Promise.resolve([]); const operations = []; + const cosmosDBAccessToken = await getAccessToken(e('AZURE_DOCUMENTDB_ENDPOINT'), e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_ID_TOKEN')); while (true) { const [timeline, artifacts] = await Promise.all([(0, retry_1.retry)(() => getPipelineTimeline()), (0, retry_1.retry)(() => getPipelineArtifacts())]); const stagesCompleted = new Set(timeline.records.filter(r => r.type === 'Stage' && r.state === 'completed' && stages.has(r.name)).map(r => r.name)); @@ -575,7 +602,7 @@ async function main() { const artifactFilePath = artifactFilePaths.filter(p => !/_manifest/.test(p))[0]; processing.add(artifact.name); const promise = new Promise((resolve, reject) => { - const worker = new node_worker_threads_1.Worker(__filename, { workerData: { artifact, artifactFilePath } }); + const worker = new node_worker_threads_1.Worker(__filename, { workerData: { artifact, artifactFilePath, cosmosDBAccessToken } }); worker.on('error', reject); worker.on('exit', code => { if (code === 0) { diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index 9774759cdbae4..37ed4232f38e5 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -12,7 +12,7 @@ import * as yauzl from 'yauzl'; import * as crypto from 'crypto'; import { retry } from './retry'; import { CosmosClient } from '@azure/cosmos'; -import { ClientSecretCredential, ClientAssertionCredential } from '@azure/identity'; +import { ClientSecretCredential } from '@azure/identity'; import * as cp from 'child_process'; import * as os from 'os'; import { Worker, isMainThread, workerData } from 'node:worker_threads'; @@ -47,6 +47,36 @@ class Temp { } } +/** + * Gets an access token converted from a WIF/OIDC id token. + * We need this since this build job takes a while to run and while id tokens live for 10 minutes only, access tokens live for 24 hours. + * Source: https://goodworkaround.com/2021/12/21/another-deep-dive-into-azure-ad-workload-identity-federation-using-github-actions/ + */ +export async function getAccessToken(endpoint: string, tenantId: string, clientId: string, idToken: string): Promise { + const body = new URLSearchParams({ + scope: `${endpoint}.default`, + client_id: clientId, + grant_type: 'client_credentials', + client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + client_assertion: encodeURIComponent(idToken) + }); + + const response = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: body.toString() + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const aadToken = await response.json(); + return aadToken.access_token; +} + interface RequestOptions { readonly body?: string; } @@ -636,7 +666,7 @@ function getRealType(type: string) { } } -async function processArtifact(artifact: Artifact, artifactFilePath: string): Promise { +async function processArtifact(artifact: Artifact, artifactFilePath: string, cosmosDBAccessToken: string): Promise { const log = (...args: any[]) => console.log(`[${artifact.name}]`, ...args); const match = /^vscode_(?[^_]+)_(?[^_]+)(?:_legacy)?_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); @@ -674,8 +704,7 @@ async function processArtifact(artifact: Artifact, artifactFilePath: string): Pr await retry(async (attempt) => { log(`Creating asset in Cosmos DB (attempt ${attempt})...`); - const aadCredentials = new ClientAssertionCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, () => Promise.resolve(process.env['AZURE_ID_TOKEN']!)); - const client = new CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), aadCredentials }); + const client = new CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT')!, tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken}`) }); const scripts = client.database('builds').container(quality).scripts; await scripts.storedProcedure('createAsset').execute('', [commit, asset, true]); }); @@ -691,8 +720,8 @@ async function processArtifact(artifact: Artifact, artifactFilePath: string): Pr // the CDN and finally update the build in Cosmos DB. async function main() { if (!isMainThread) { - const { artifact, artifactFilePath } = workerData; - await processArtifact(artifact, artifactFilePath); + const { artifact, artifactFilePath, cosmosDBAccessToken } = workerData; + await processArtifact(artifact, artifactFilePath, cosmosDBAccessToken); return; } @@ -713,6 +742,7 @@ async function main() { let resultPromise = Promise.resolve[]>([]); const operations: { name: string; operation: Promise }[] = []; + const cosmosDBAccessToken = await getAccessToken(e('AZURE_DOCUMENTDB_ENDPOINT')!, e('AZURE_TENANT_ID')!, e('AZURE_CLIENT_ID')!, e('AZURE_ID_TOKEN')!); while (true) { const [timeline, artifacts] = await Promise.all([retry(() => getPipelineTimeline()), retry(() => getPipelineArtifacts())]); @@ -754,7 +784,7 @@ async function main() { processing.add(artifact.name); const promise = new Promise((resolve, reject) => { - const worker = new Worker(__filename, { workerData: { artifact, artifactFilePath } }); + const worker = new Worker(__filename, { workerData: { artifact, artifactFilePath, cosmosDBAccessToken } }); worker.on('error', reject); worker.on('exit', code => { if (code === 0) { diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index a196a1117f83b..a053d96033c23 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -143,7 +143,7 @@ variables: - name: PRSS_PROVISION_TENANT_ID value: 72f988bf-86f1-41af-91ab-2d7cd011db47 - name: AZURE_DOCUMENTDB_ENDPOINT - value: https://vscode.documents.azure.com:443/ + value: https://vscode.documents.azure.com/ - name: VSCODE_MIXIN_REPO value: microsoft/vscode-distro - name: skipComponentGovernanceDetection