From 8aaa591e67e071b253c8f9834be1c3ee6cf077d9 Mon Sep 17 00:00:00 2001 From: Alex Torres <95253638+AlexVTor@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:50:26 -0700 Subject: [PATCH] Bugfix: Match service connection using feedId/ProjID (#19732) * Bugfix: Parse service connection using feedId/ProjID * Bump task version * Add Type matching Co-authored-by: Jonathan Myers <11822817+jmyersmsft@users.noreply.github.com> * updated generated files * Verifying service connection matches feed * Limit search to subset of service connections * Bump task version * Removed unused var * Applied review suggestions * rebuild / removed unused + comment --------- Co-authored-by: Jonathan Myers <11822817+jmyersmsft@users.noreply.github.com> --- Tasks/NuGetCommandV2/nugetpublisher.ts | 64 ++++++++++++++++--- Tasks/NuGetCommandV2/task.json | 2 +- Tasks/NuGetCommandV2/task.loc.json | 2 +- _generated/NuGetCommandV2.versionmap.txt | 4 +- _generated/NuGetCommandV2/nugetpublisher.ts | 64 ++++++++++++++++--- _generated/NuGetCommandV2/task.json | 6 +- _generated/NuGetCommandV2/task.loc.json | 6 +- .../NuGetCommandV2_Node20/nugetpublisher.ts | 64 ++++++++++++++++--- _generated/NuGetCommandV2_Node20/task.json | 6 +- .../NuGetCommandV2_Node20/task.loc.json | 6 +- 10 files changed, 178 insertions(+), 46 deletions(-) diff --git a/Tasks/NuGetCommandV2/nugetpublisher.ts b/Tasks/NuGetCommandV2/nugetpublisher.ts index 80a7040cdc80..911ea17df01f 100644 --- a/Tasks/NuGetCommandV2/nugetpublisher.ts +++ b/Tasks/NuGetCommandV2/nugetpublisher.ts @@ -15,6 +15,8 @@ import * as vstsNuGetPushToolRunner from "./Common/VstsNuGetPushToolRunner"; import * as vstsNuGetPushToolUtilities from "./Common/VstsNuGetPushToolUtilities"; import { getProjectAndFeedIdFromInputParam } from 'azure-pipelines-tasks-packaging-common/util'; import { logError } from 'azure-pipelines-tasks-packaging-common/util'; +import { WebRequest, WebResponse, sendRequest } from 'azure-pipelines-tasks-utility-common/restutilities'; + class PublishOptions implements INuGetCommandOptions { constructor( @@ -112,7 +114,7 @@ export async function run(nuGetPath: string): Promise { let accessToken; let feed; const isInternalFeed: boolean = nugetFeedType === "internal"; - accessToken = getAccessToken(isInternalFeed); + accessToken = await getAccessToken(isInternalFeed, urlPrefixes); const quirks = await ngToolRunner.getNuGetQuirksAsync(nuGetPath); // Clauses ordered in this way to avoid short-circuit evaluation, so the debug info printed by the functions @@ -414,9 +416,9 @@ function shouldUseVstsNuGetPush(isInternalFeed: boolean, conflictsAllowed: boole return false; } -function getAccessToken(isInternalFeed: boolean): string{ - let accessToken: string; +async function getAccessToken(isInternalFeed: boolean, uriPrefixes: any): Promise{ let allowServiceConnection = tl.getVariable('PUBLISH_VIA_SERVICE_CONNECTION'); + let accessToken: string; if(allowServiceConnection) { let endpoint = tl.getInput('externalEndpoint', false); @@ -440,16 +442,25 @@ function getAccessToken(isInternalFeed: boolean): string{ tl.debug("Checking for auth from Cred Provider."); const feed = getProjectAndFeedIdFromInputParam('feedPublish'); const JsonEndpointsString = process.env["VSS_NUGET_EXTERNAL_FEED_ENDPOINTS"]; + if (JsonEndpointsString) { + tl.debug(`Feed details ${feed.feedId} ${feed.projectId}`); tl.debug(`Endpoints found: ${JsonEndpointsString}`); let endpointsArray: { endpointCredentials: EndpointCredentials[] } = JSON.parse(JsonEndpointsString); - tl.debug(`Feed details ${feed.feedId} ${feed.projectId}`); - - for (let endpoint_in = 0; endpoint_in < endpointsArray.endpointCredentials.length; endpoint_in++) { - if (endpointsArray.endpointCredentials[endpoint_in].endpoint.search(feed.feedName) != -1) { - tl.debug(`Endpoint Credentials found for ${feed.feedName}`); - accessToken = endpointsArray.endpointCredentials[endpoint_in].password; + let matchingEndpoint: EndpointCredentials; + for (const e of endpointsArray.endpointCredentials) { + for (const prefix of uriPrefixes) { + if (e.endpoint.toUpperCase().startsWith(prefix.toUpperCase())) { + let isServiceConnectionValid = await tryServiceConnection(e, feed); + if (isServiceConnectionValid) { + matchingEndpoint = e; + break; + } + } + } + if (matchingEndpoint) { + accessToken = matchingEndpoint.password; break; } } @@ -466,4 +477,37 @@ function getAccessToken(isInternalFeed: boolean): string{ } return accessToken; -} \ No newline at end of file +} + +async function tryServiceConnection(endpoint: EndpointCredentials, feed: any) : Promise +{ + // Create request + const request = new WebRequest(); + const token64 = Buffer.from(`${endpoint.username}:${endpoint.password}`).toString('base64'); + request.uri = endpoint.endpoint; + request.method = 'GET'; + request.headers = { + "Content-Type": "application/json", + "Authorization": "Basic " + token64 + }; + + const response = await sendRequest(request); + + if(response.statusCode == 200) { + if(response.body) { + for (const entry of response.body.resources) { + if (entry['@type'] === 'AzureDevOpsProjectId' && entry['label'].toUpperCase() !== feed.projectId.toUpperCase()) + { + return false; + } + if (entry['@type'] === 'VssFeedId' && entry['label'].toUpperCase() !== feed.feedId.toUpperCase()) + { + return false; + } + } + // We found matches in feedId and projectId, return the service connection + return true; + } + } + return false; +} diff --git a/Tasks/NuGetCommandV2/task.json b/Tasks/NuGetCommandV2/task.json index 7f043938a055..d28c0e7d7f71 100644 --- a/Tasks/NuGetCommandV2/task.json +++ b/Tasks/NuGetCommandV2/task.json @@ -10,7 +10,7 @@ "version": { "Major": 2, "Minor": 238, - "Patch": 0 + "Patch": 1 }, "runsOn": [ "Agent", diff --git a/Tasks/NuGetCommandV2/task.loc.json b/Tasks/NuGetCommandV2/task.loc.json index c8fd10178629..74fce5bd1185 100644 --- a/Tasks/NuGetCommandV2/task.loc.json +++ b/Tasks/NuGetCommandV2/task.loc.json @@ -10,7 +10,7 @@ "version": { "Major": 2, "Minor": 238, - "Patch": 0 + "Patch": 1 }, "runsOn": [ "Agent", diff --git a/_generated/NuGetCommandV2.versionmap.txt b/_generated/NuGetCommandV2.versionmap.txt index 71819a7c2e8d..03fcb7e50ea9 100644 --- a/_generated/NuGetCommandV2.versionmap.txt +++ b/_generated/NuGetCommandV2.versionmap.txt @@ -1,2 +1,2 @@ -Default|2.238.0 -Node20_229_1|2.238.1 +Default|2.238.1 +Node20_229_1|2.238.2 diff --git a/_generated/NuGetCommandV2/nugetpublisher.ts b/_generated/NuGetCommandV2/nugetpublisher.ts index 80a7040cdc80..911ea17df01f 100644 --- a/_generated/NuGetCommandV2/nugetpublisher.ts +++ b/_generated/NuGetCommandV2/nugetpublisher.ts @@ -15,6 +15,8 @@ import * as vstsNuGetPushToolRunner from "./Common/VstsNuGetPushToolRunner"; import * as vstsNuGetPushToolUtilities from "./Common/VstsNuGetPushToolUtilities"; import { getProjectAndFeedIdFromInputParam } from 'azure-pipelines-tasks-packaging-common/util'; import { logError } from 'azure-pipelines-tasks-packaging-common/util'; +import { WebRequest, WebResponse, sendRequest } from 'azure-pipelines-tasks-utility-common/restutilities'; + class PublishOptions implements INuGetCommandOptions { constructor( @@ -112,7 +114,7 @@ export async function run(nuGetPath: string): Promise { let accessToken; let feed; const isInternalFeed: boolean = nugetFeedType === "internal"; - accessToken = getAccessToken(isInternalFeed); + accessToken = await getAccessToken(isInternalFeed, urlPrefixes); const quirks = await ngToolRunner.getNuGetQuirksAsync(nuGetPath); // Clauses ordered in this way to avoid short-circuit evaluation, so the debug info printed by the functions @@ -414,9 +416,9 @@ function shouldUseVstsNuGetPush(isInternalFeed: boolean, conflictsAllowed: boole return false; } -function getAccessToken(isInternalFeed: boolean): string{ - let accessToken: string; +async function getAccessToken(isInternalFeed: boolean, uriPrefixes: any): Promise{ let allowServiceConnection = tl.getVariable('PUBLISH_VIA_SERVICE_CONNECTION'); + let accessToken: string; if(allowServiceConnection) { let endpoint = tl.getInput('externalEndpoint', false); @@ -440,16 +442,25 @@ function getAccessToken(isInternalFeed: boolean): string{ tl.debug("Checking for auth from Cred Provider."); const feed = getProjectAndFeedIdFromInputParam('feedPublish'); const JsonEndpointsString = process.env["VSS_NUGET_EXTERNAL_FEED_ENDPOINTS"]; + if (JsonEndpointsString) { + tl.debug(`Feed details ${feed.feedId} ${feed.projectId}`); tl.debug(`Endpoints found: ${JsonEndpointsString}`); let endpointsArray: { endpointCredentials: EndpointCredentials[] } = JSON.parse(JsonEndpointsString); - tl.debug(`Feed details ${feed.feedId} ${feed.projectId}`); - - for (let endpoint_in = 0; endpoint_in < endpointsArray.endpointCredentials.length; endpoint_in++) { - if (endpointsArray.endpointCredentials[endpoint_in].endpoint.search(feed.feedName) != -1) { - tl.debug(`Endpoint Credentials found for ${feed.feedName}`); - accessToken = endpointsArray.endpointCredentials[endpoint_in].password; + let matchingEndpoint: EndpointCredentials; + for (const e of endpointsArray.endpointCredentials) { + for (const prefix of uriPrefixes) { + if (e.endpoint.toUpperCase().startsWith(prefix.toUpperCase())) { + let isServiceConnectionValid = await tryServiceConnection(e, feed); + if (isServiceConnectionValid) { + matchingEndpoint = e; + break; + } + } + } + if (matchingEndpoint) { + accessToken = matchingEndpoint.password; break; } } @@ -466,4 +477,37 @@ function getAccessToken(isInternalFeed: boolean): string{ } return accessToken; -} \ No newline at end of file +} + +async function tryServiceConnection(endpoint: EndpointCredentials, feed: any) : Promise +{ + // Create request + const request = new WebRequest(); + const token64 = Buffer.from(`${endpoint.username}:${endpoint.password}`).toString('base64'); + request.uri = endpoint.endpoint; + request.method = 'GET'; + request.headers = { + "Content-Type": "application/json", + "Authorization": "Basic " + token64 + }; + + const response = await sendRequest(request); + + if(response.statusCode == 200) { + if(response.body) { + for (const entry of response.body.resources) { + if (entry['@type'] === 'AzureDevOpsProjectId' && entry['label'].toUpperCase() !== feed.projectId.toUpperCase()) + { + return false; + } + if (entry['@type'] === 'VssFeedId' && entry['label'].toUpperCase() !== feed.feedId.toUpperCase()) + { + return false; + } + } + // We found matches in feedId and projectId, return the service connection + return true; + } + } + return false; +} diff --git a/_generated/NuGetCommandV2/task.json b/_generated/NuGetCommandV2/task.json index 51f58390a891..bf27a7dd70b5 100644 --- a/_generated/NuGetCommandV2/task.json +++ b/_generated/NuGetCommandV2/task.json @@ -10,7 +10,7 @@ "version": { "Major": 2, "Minor": 238, - "Patch": 0 + "Patch": 1 }, "runsOn": [ "Agent", @@ -546,7 +546,7 @@ "Warning_UnsupportedServiceConnectionAuth": "The service connection does not use a supported authentication method. Please use a service connection with personal access token based auth." }, "_buildConfigMapping": { - "Default": "2.238.0", - "Node20_229_1": "2.238.1" + "Default": "2.238.1", + "Node20_229_1": "2.238.2" } } \ No newline at end of file diff --git a/_generated/NuGetCommandV2/task.loc.json b/_generated/NuGetCommandV2/task.loc.json index 23c4b87fb697..90e195c347ad 100644 --- a/_generated/NuGetCommandV2/task.loc.json +++ b/_generated/NuGetCommandV2/task.loc.json @@ -10,7 +10,7 @@ "version": { "Major": 2, "Minor": 238, - "Patch": 0 + "Patch": 1 }, "runsOn": [ "Agent", @@ -546,7 +546,7 @@ "Warning_UnsupportedServiceConnectionAuth": "ms-resource:loc.messages.Warning_UnsupportedServiceConnectionAuth" }, "_buildConfigMapping": { - "Default": "2.238.0", - "Node20_229_1": "2.238.1" + "Default": "2.238.1", + "Node20_229_1": "2.238.2" } } \ No newline at end of file diff --git a/_generated/NuGetCommandV2_Node20/nugetpublisher.ts b/_generated/NuGetCommandV2_Node20/nugetpublisher.ts index 80a7040cdc80..911ea17df01f 100644 --- a/_generated/NuGetCommandV2_Node20/nugetpublisher.ts +++ b/_generated/NuGetCommandV2_Node20/nugetpublisher.ts @@ -15,6 +15,8 @@ import * as vstsNuGetPushToolRunner from "./Common/VstsNuGetPushToolRunner"; import * as vstsNuGetPushToolUtilities from "./Common/VstsNuGetPushToolUtilities"; import { getProjectAndFeedIdFromInputParam } from 'azure-pipelines-tasks-packaging-common/util'; import { logError } from 'azure-pipelines-tasks-packaging-common/util'; +import { WebRequest, WebResponse, sendRequest } from 'azure-pipelines-tasks-utility-common/restutilities'; + class PublishOptions implements INuGetCommandOptions { constructor( @@ -112,7 +114,7 @@ export async function run(nuGetPath: string): Promise { let accessToken; let feed; const isInternalFeed: boolean = nugetFeedType === "internal"; - accessToken = getAccessToken(isInternalFeed); + accessToken = await getAccessToken(isInternalFeed, urlPrefixes); const quirks = await ngToolRunner.getNuGetQuirksAsync(nuGetPath); // Clauses ordered in this way to avoid short-circuit evaluation, so the debug info printed by the functions @@ -414,9 +416,9 @@ function shouldUseVstsNuGetPush(isInternalFeed: boolean, conflictsAllowed: boole return false; } -function getAccessToken(isInternalFeed: boolean): string{ - let accessToken: string; +async function getAccessToken(isInternalFeed: boolean, uriPrefixes: any): Promise{ let allowServiceConnection = tl.getVariable('PUBLISH_VIA_SERVICE_CONNECTION'); + let accessToken: string; if(allowServiceConnection) { let endpoint = tl.getInput('externalEndpoint', false); @@ -440,16 +442,25 @@ function getAccessToken(isInternalFeed: boolean): string{ tl.debug("Checking for auth from Cred Provider."); const feed = getProjectAndFeedIdFromInputParam('feedPublish'); const JsonEndpointsString = process.env["VSS_NUGET_EXTERNAL_FEED_ENDPOINTS"]; + if (JsonEndpointsString) { + tl.debug(`Feed details ${feed.feedId} ${feed.projectId}`); tl.debug(`Endpoints found: ${JsonEndpointsString}`); let endpointsArray: { endpointCredentials: EndpointCredentials[] } = JSON.parse(JsonEndpointsString); - tl.debug(`Feed details ${feed.feedId} ${feed.projectId}`); - - for (let endpoint_in = 0; endpoint_in < endpointsArray.endpointCredentials.length; endpoint_in++) { - if (endpointsArray.endpointCredentials[endpoint_in].endpoint.search(feed.feedName) != -1) { - tl.debug(`Endpoint Credentials found for ${feed.feedName}`); - accessToken = endpointsArray.endpointCredentials[endpoint_in].password; + let matchingEndpoint: EndpointCredentials; + for (const e of endpointsArray.endpointCredentials) { + for (const prefix of uriPrefixes) { + if (e.endpoint.toUpperCase().startsWith(prefix.toUpperCase())) { + let isServiceConnectionValid = await tryServiceConnection(e, feed); + if (isServiceConnectionValid) { + matchingEndpoint = e; + break; + } + } + } + if (matchingEndpoint) { + accessToken = matchingEndpoint.password; break; } } @@ -466,4 +477,37 @@ function getAccessToken(isInternalFeed: boolean): string{ } return accessToken; -} \ No newline at end of file +} + +async function tryServiceConnection(endpoint: EndpointCredentials, feed: any) : Promise +{ + // Create request + const request = new WebRequest(); + const token64 = Buffer.from(`${endpoint.username}:${endpoint.password}`).toString('base64'); + request.uri = endpoint.endpoint; + request.method = 'GET'; + request.headers = { + "Content-Type": "application/json", + "Authorization": "Basic " + token64 + }; + + const response = await sendRequest(request); + + if(response.statusCode == 200) { + if(response.body) { + for (const entry of response.body.resources) { + if (entry['@type'] === 'AzureDevOpsProjectId' && entry['label'].toUpperCase() !== feed.projectId.toUpperCase()) + { + return false; + } + if (entry['@type'] === 'VssFeedId' && entry['label'].toUpperCase() !== feed.feedId.toUpperCase()) + { + return false; + } + } + // We found matches in feedId and projectId, return the service connection + return true; + } + } + return false; +} diff --git a/_generated/NuGetCommandV2_Node20/task.json b/_generated/NuGetCommandV2_Node20/task.json index fd7a5bb2df7b..d21c06125f7e 100644 --- a/_generated/NuGetCommandV2_Node20/task.json +++ b/_generated/NuGetCommandV2_Node20/task.json @@ -10,7 +10,7 @@ "version": { "Major": 2, "Minor": 238, - "Patch": 1 + "Patch": 2 }, "runsOn": [ "Agent", @@ -550,7 +550,7 @@ "Warning_UnsupportedServiceConnectionAuth": "The service connection does not use a supported authentication method. Please use a service connection with personal access token based auth." }, "_buildConfigMapping": { - "Default": "2.238.0", - "Node20_229_1": "2.238.1" + "Default": "2.238.1", + "Node20_229_1": "2.238.2" } } \ No newline at end of file diff --git a/_generated/NuGetCommandV2_Node20/task.loc.json b/_generated/NuGetCommandV2_Node20/task.loc.json index 72ba882763aa..35e20aefc2ae 100644 --- a/_generated/NuGetCommandV2_Node20/task.loc.json +++ b/_generated/NuGetCommandV2_Node20/task.loc.json @@ -10,7 +10,7 @@ "version": { "Major": 2, "Minor": 238, - "Patch": 1 + "Patch": 2 }, "runsOn": [ "Agent", @@ -550,7 +550,7 @@ "Warning_UnsupportedServiceConnectionAuth": "ms-resource:loc.messages.Warning_UnsupportedServiceConnectionAuth" }, "_buildConfigMapping": { - "Default": "2.238.0", - "Node20_229_1": "2.238.1" + "Default": "2.238.1", + "Node20_229_1": "2.238.2" } } \ No newline at end of file