From b13064a8c8eb7d370f95762172d52204021fe376 Mon Sep 17 00:00:00 2001 From: Zach Renner Date: Tue, 16 Jul 2019 10:17:21 -0700 Subject: [PATCH] NuGetAuthenticate task --- .github/CODEOWNERS | 4 + .../resources.resjson/en-US/resources.resjson | 13 ++ Tasks/Common/artifacts-common/Tests/L0.ts | 9 + .../Tests/credentialProviderUtilsTests.ts | 110 ++++++++++ .../Tests/packagingAccessMappingUtilsTests.ts | 132 ++++++++++++ .../Tests/serviceConnectionUtilsTests.ts | 158 ++++++++++++++ .../artifacts-common/connectionDataUtils.ts | 56 +++++ .../credentialProviderUtils.ts | 195 ++++++++++++++++++ Tasks/Common/artifacts-common/module.json | 15 ++ .../Common/artifacts-common/package-lock.json | 155 ++++++++++++++ Tasks/Common/artifacts-common/package.json | 23 +++ .../packagingAccessMappingUtils.ts | 65 ++++++ Tasks/Common/artifacts-common/protocols.ts | 20 ++ Tasks/Common/artifacts-common/retryUtils.ts | 22 ++ .../serviceConnectionUtils.ts | 128 ++++++++++++ Tasks/Common/artifacts-common/telemetry.ts | 47 +++++ Tasks/Common/artifacts-common/tsconfig.json | 12 ++ Tasks/Common/artifacts-common/webapi.ts | 25 +++ .../resources.resjson/en-US/resources.resjson | 10 + Tasks/NuGetAuthenticateV0/icon.png | Bin 0 -> 1621 bytes Tasks/NuGetAuthenticateV0/icon.svg | 10 + Tasks/NuGetAuthenticateV0/main.ts | 30 +++ Tasks/NuGetAuthenticateV0/make.json | 25 +++ Tasks/NuGetAuthenticateV0/package-lock.json | 167 +++++++++++++++ Tasks/NuGetAuthenticateV0/package.json | 28 +++ Tasks/NuGetAuthenticateV0/task.json | 48 +++++ Tasks/NuGetAuthenticateV0/task.loc.json | 47 +++++ Tasks/NuGetAuthenticateV0/tsconfig.json | 6 + make-options.json | 1 + 29 files changed, 1561 insertions(+) create mode 100644 Tasks/Common/artifacts-common/Strings/resources.resjson/en-US/resources.resjson create mode 100644 Tasks/Common/artifacts-common/Tests/L0.ts create mode 100644 Tasks/Common/artifacts-common/Tests/credentialProviderUtilsTests.ts create mode 100644 Tasks/Common/artifacts-common/Tests/packagingAccessMappingUtilsTests.ts create mode 100644 Tasks/Common/artifacts-common/Tests/serviceConnectionUtilsTests.ts create mode 100644 Tasks/Common/artifacts-common/connectionDataUtils.ts create mode 100644 Tasks/Common/artifacts-common/credentialProviderUtils.ts create mode 100644 Tasks/Common/artifacts-common/module.json create mode 100644 Tasks/Common/artifacts-common/package-lock.json create mode 100644 Tasks/Common/artifacts-common/package.json create mode 100644 Tasks/Common/artifacts-common/packagingAccessMappingUtils.ts create mode 100644 Tasks/Common/artifacts-common/protocols.ts create mode 100644 Tasks/Common/artifacts-common/retryUtils.ts create mode 100644 Tasks/Common/artifacts-common/serviceConnectionUtils.ts create mode 100644 Tasks/Common/artifacts-common/telemetry.ts create mode 100644 Tasks/Common/artifacts-common/tsconfig.json create mode 100644 Tasks/Common/artifacts-common/webapi.ts create mode 100644 Tasks/NuGetAuthenticateV0/Strings/resources.resjson/en-US/resources.resjson create mode 100644 Tasks/NuGetAuthenticateV0/icon.png create mode 100644 Tasks/NuGetAuthenticateV0/icon.svg create mode 100644 Tasks/NuGetAuthenticateV0/main.ts create mode 100644 Tasks/NuGetAuthenticateV0/make.json create mode 100644 Tasks/NuGetAuthenticateV0/package-lock.json create mode 100644 Tasks/NuGetAuthenticateV0/package.json create mode 100644 Tasks/NuGetAuthenticateV0/task.json create mode 100644 Tasks/NuGetAuthenticateV0/task.loc.json create mode 100644 Tasks/NuGetAuthenticateV0/tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5c752680f79b..906e4e0fed09 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -83,6 +83,8 @@ Tasks/CmdLineV2/* @bryanmacfarlane Tasks/CocoaPodsV0/* @madhurig +Tasks/Common/artifacts-common/* @zjrunner @zarenner @shubham90 + Tasks/Common/AzureRmDeploy-common/* @vincentdass @SumiranAgg Tasks/Common/Deployment/* @bishal-pdMSFT @chshrikh @@ -247,6 +249,8 @@ Tasks/NuGetV0/* @zjrunner Tasks/NuGetCommandV2/* @zjrunner @jotaylo +Tasks/NuGetAuthenticateV0/* @zjrunner @zarenner + Tasks/NuGetPackagerV0/* @zjrunner Tasks/NuGetPublisherV0/* @zjrunner diff --git a/Tasks/Common/artifacts-common/Strings/resources.resjson/en-US/resources.resjson b/Tasks/Common/artifacts-common/Strings/resources.resjson/en-US/resources.resjson new file mode 100644 index 000000000000..89991a1d3fa0 --- /dev/null +++ b/Tasks/Common/artifacts-common/Strings/resources.resjson/en-US/resources.resjson @@ -0,0 +1,13 @@ +{ + "loc.messages.CredProvider_AlreadyInstalled": "The credential provider is already installed. If there are package related authentication failures in later steps, consider setting this task's input 'forceReinstallCredentialProvider' to true to force reinstallation.", + "loc.messages.CredProvider_Error_FailedCopy": "Failed to copy from '%s' to '%s' while installing the credential provider. Ensure that the destination directory is not in use and that the agent account has permission to write to it.", + "loc.messages.CredProvider_Error_FailedRemoveDir": "Failed to remove the directory '%s' while installing the credential provider. Ensure that this directory is not in use and that the agent account has permission to delete this directory.", + "loc.messages.CredProvider_Error_InvalidServiceConnection": "The service connection for '%s' is not valid.", + "loc.messages.CredProvider_Error_InvalidServiceConnection_ApiKey": "The service connection for '%s' is not valid. ApiKey service connections are not supported in this task. Instead, use -ApiKey (NuGet) or --api-key (dotnet) when invoking the tool itself. See the task documentation for more details.", + "loc.messages.CredProvider_InstallingNetCoreTo": "Installing the Azure Artifacts Credential Provider (.NET Core) to '%s'. This credential provider is compatible with dotnet SDK 2.1.400 or later.", + "loc.messages.CredProvider_InstallingNetFxTo": "Installing the Azure Artifacts Credential Provider (.NET Framework) to '%s'. This credential provider is compatible with nuget.exe 4.8.0.5385 or later, and MSBuild 15.8.166.59604 or later.", + "loc.messages.CredProvider_SettingUpForOrgFeeds": "Setting up the credential provider to use the identity '%s' for feeds in your organization/collection starting with:", + "loc.messages.CredProvider_SettingUpForServiceConnections": "Setting up the credential provider for these service connections:", + "loc.messages.ServiceConnections_Error_FailedToParseServiceEndpoint_MissingParameter": "Failed to parse the service endpoint '%s' because it was missing the parameter '%s'", + "loc.messages.ServiceConnections_Error_FailedToParseServiceEndpoint_BadScheme": "Failed to parse the service endpoint '%s' because the auth scheme '%s' was invalid" +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/Tests/L0.ts b/Tasks/Common/artifacts-common/Tests/L0.ts new file mode 100644 index 000000000000..cc17fd910954 --- /dev/null +++ b/Tasks/Common/artifacts-common/Tests/L0.ts @@ -0,0 +1,9 @@ +import { packagingAccessMappingUtilsTests } from "./packagingAccessMappingUtilsTests"; +import { credentialProviderUtilsTests } from "./credentialProviderUtilsTests"; +import { serviceConnectionUtilsTests } from "./serviceConnectionUtilsTests"; + +describe("artifacts-common suite", function() { + describe("packagingAccessMappingUtils", packagingAccessMappingUtilsTests); + describe("credentialProviderUtils", credentialProviderUtilsTests); + describe("serviceConnectionUtils", serviceConnectionUtilsTests); +}); diff --git a/Tasks/Common/artifacts-common/Tests/credentialProviderUtilsTests.ts b/Tasks/Common/artifacts-common/Tests/credentialProviderUtilsTests.ts new file mode 100644 index 000000000000..7a839ceef407 --- /dev/null +++ b/Tasks/Common/artifacts-common/Tests/credentialProviderUtilsTests.ts @@ -0,0 +1,110 @@ +import * as assert from "assert"; +import { buildExternalFeedEndpointsJson } from "../credentialProviderUtils"; +import { ServiceConnectionAuthType, TokenServiceConnection, ApiKeyServiceConnection, UsernamePasswordServiceConnection } from "../serviceConnectionUtils"; + +export function credentialProviderUtilsTests() { + + beforeEach(() => { + }); + + afterEach(() => { + }); + + it("buildExternalFeedEndpointsJson null returns null", (done: MochaDone) => { + assert.equal(buildExternalFeedEndpointsJson(null), null); + done(); + }); + + it("buildExternalFeedEndpointsJson empty returns null", (done: MochaDone) => { + assert.equal(buildExternalFeedEndpointsJson([]), null); + done(); + }); + + it("buildExternalFeedEndpointsJson token", (done: MochaDone) => { + const json = buildExternalFeedEndpointsJson([ + { + packageSource: { + uri: "https://contoso.com/nuget/v3/index.json" + }, + authType: ServiceConnectionAuthType.Token, + token: "sometoken" + } + ]) + + assert.equal(json, "{\"endpointCredentials\":[{\"endpoint\":\"https://contoso.com/nuget/v3/index.json\",\"password\":\"sometoken\"}]}"); + + done(); + }); + + it("buildExternalFeedEndpointsJson usernamepassword", (done: MochaDone) => { + const json = buildExternalFeedEndpointsJson([ + { + packageSource: { + uri: "https://fabrikam.com/nuget/v3/index.json" + }, + authType: ServiceConnectionAuthType.UsernamePassword, + username: "someusername", + password: "somepassword" + } + ]) + + assert.equal(json, "{\"endpointCredentials\":[{\"endpoint\":\"https://fabrikam.com/nuget/v3/index.json\",\"username\":\"someusername\",\"password\":\"somepassword\"}]}"); + + done(); + }); + + it("buildExternalFeedEndpointsJson token + usernamepassword", (done: MochaDone) => { + const json = buildExternalFeedEndpointsJson([ + { + packageSource: { + uri: "https://contoso.com/nuget/v3/index.json" + }, + authType: ServiceConnectionAuthType.Token, + token: "sometoken" + }, + { + packageSource: { + uri: "https://fabrikam.com/nuget/v3/index.json" + }, + authType: ServiceConnectionAuthType.UsernamePassword, + username: "someusername", + password: "somepassword" + } + ]) + + assert.equal(json, "{\"endpointCredentials\":[{\"endpoint\":\"https://contoso.com/nuget/v3/index.json\",\"password\":\"sometoken\"},{\"endpoint\":\"https://fabrikam.com/nuget/v3/index.json\",\"username\":\"someusername\",\"password\":\"somepassword\"}]}"); + + done(); + }); + + it("buildExternalFeedEndpointsJson apikey throws", (done: MochaDone) => { + assert.throws(() => { + buildExternalFeedEndpointsJson([ + { + packageSource: { + uri: "https://contoso.com/nuget/v3/index.json" + }, + authType: ServiceConnectionAuthType.ApiKey, + apiKey: "someapikey" + } + ]) + }); + + done(); + }); + + it("buildExternalFeedEndpointsJson otherauthtype throws", (done: MochaDone) => { + assert.throws(() => { + buildExternalFeedEndpointsJson([ + { + packageSource: { + uri: "https://contoso.com/nuget/v3/index.json" + }, + authType: "unsupportedauthtype", + } + ]) + }); + + done(); + }); +} diff --git a/Tasks/Common/artifacts-common/Tests/packagingAccessMappingUtilsTests.ts b/Tasks/Common/artifacts-common/Tests/packagingAccessMappingUtilsTests.ts new file mode 100644 index 000000000000..98fb070ea552 --- /dev/null +++ b/Tasks/Common/artifacts-common/Tests/packagingAccessMappingUtilsTests.ts @@ -0,0 +1,132 @@ +import * as assert from "assert"; +import { getPackagingAccessMappings, PackagingAccessMapping } from "../packagingAccessMappingUtils"; + +export function packagingAccessMappingUtilsTests() { + const standardDefaultAccessMappingMoniker = "PublicAccessMapping"; + + function getAccessMappings(newDomain) { + let publicAccessPoint = "https://contoso.pkgs.visualstudio.com/" + if (newDomain) { + publicAccessPoint = "https://pkgs.dev.azure.com/contoso/"; + } + return [ + { + displayName: "Host Guid Access Mapping", + moniker: "HostGuidAccessMapping", + accessPoint: "https://pkgsprodscussu0.pkgs.visualstudio.com/", + serviceOwner: "00000000-0000-0000-0000-000000000000", + virtualDirectory: "Aa731d82f-a042-44ad-a928-61581ea38485" + }, + { + displayName: "Public Access Mapping", + moniker: "PublicAccessMapping", + accessPoint: publicAccessPoint, + serviceOwner: "00000000-0000-0000-0000-000000000000", + virtualDirectory: "" + }, + { + displayName: "VSTS Access Mapping", + moniker: "VstsAccessMapping", + accessPoint: "https://contoso.pkgs.visualstudio.com/", + serviceOwner: "00000000-0000-0000-0000-000000000000", + virtualDirectory: "" + }, + { + displayName: "Codex Access Mapping", + moniker: "CodexAccessMapping", + accessPoint: "https://pkgs.dev.azure.com/contoso/", + serviceOwner: "00000000-0000-0000-0000-000000000000", + virtualDirectory: "" + }]; + } + + + beforeEach(() => { + }); + + afterEach(() => { + }); + + it("getPackagingAccessMappings pkgs.visualstudio.com default", (done: MochaDone) => { + const mappings = getPackagingAccessMappings({ + defaultAccessMappingMoniker: standardDefaultAccessMappingMoniker, + accessMappings: getAccessMappings(false)}); + + assert.deepEqual(mappings, [ + { + uri: "https://pkgsprodscussu0.pkgs.visualstudio.com/Aa731d82f-a042-44ad-a928-61581ea38485/", + isPublic: false, + isDefault: false + }, + { + uri: "https://contoso.pkgs.visualstudio.com/", + isPublic: true, + isDefault: true + }, + { + uri: "https://contoso.pkgs.visualstudio.com/", + isPublic: true, + isDefault: false + }, + { + uri: "https://pkgs.dev.azure.com/contoso/", + isPublic: true, + isDefault: false + } + ]); + + done(); + }); + + it("getPackagingAccessMappings pkgs.dev.azure.com default", (done: MochaDone) => { + const mappings = getPackagingAccessMappings({ + defaultAccessMappingMoniker: standardDefaultAccessMappingMoniker, + accessMappings: getAccessMappings(true)}); + + assert.deepEqual(mappings, [ + { + uri: "https://pkgsprodscussu0.pkgs.visualstudio.com/Aa731d82f-a042-44ad-a928-61581ea38485/", + isPublic: false, + isDefault: false + }, + { + uri: "https://pkgs.dev.azure.com/contoso/", + isPublic: true, + isDefault: true + }, + { + uri: "https://contoso.pkgs.visualstudio.com/", + isPublic: true, + isDefault: false + }, + { + uri: "https://pkgs.dev.azure.com/contoso/", + isPublic: true, + isDefault: false + } + ]); + + done(); + }); + + it("getPackagingAccessMappings adds trailing slash if missing", (done: MochaDone) => { + const mappings = getPackagingAccessMappings({ + defaultAccessMappingMoniker: standardDefaultAccessMappingMoniker, + accessMappings: [ + { + moniker: "MissingSlashAccessMapping", + accessPoint: "http://pkgs.dev.azure.com/contoso" + } + ]}); + + assert.deepEqual(mappings, [ + { + uri: "http://pkgs.dev.azure.com/contoso/", + isPublic: false, + isDefault: false + } + ]); + + done(); + }); +} diff --git a/Tasks/Common/artifacts-common/Tests/serviceConnectionUtilsTests.ts b/Tasks/Common/artifacts-common/Tests/serviceConnectionUtilsTests.ts new file mode 100644 index 000000000000..a5a27679a007 --- /dev/null +++ b/Tasks/Common/artifacts-common/Tests/serviceConnectionUtilsTests.ts @@ -0,0 +1,158 @@ +import * as assert from "assert"; +import * as mockery from "mockery"; +import { EndpointAuthorization } from "azure-pipelines-task-lib"; +import { ServiceConnectionAuthType, TokenServiceConnection, UsernamePasswordServiceConnection } from "../serviceConnectionUtils"; + +export function serviceConnectionUtilsTests() { + + const serviceConnectionsKey = "someProtocolServiceConnections"; + + before(() => { + mockery.disable(); // needed to ensure that we can mock vsts-task-lib/task + mockery.enable({ + useCleanCache: true, + warnOnUnregistered: true + } as mockery.MockeryEnableArgs); + }); + + after(() => { + mockery.disable(); + }); + + beforeEach(() => { + mockery.resetCache(); + }); + + afterEach(() => { + mockery.deregisterAll(); + }); + + it("getPackagingServiceConnections null returns empty", (done: MochaDone) => { + let mockTask = { + getDelimitedInput: (key) => { + return null; + } + }; + mockery.registerMock('azure-pipelines-task-lib/task', mockTask); + + let serviceConnectionUtilsWithMocks = require("../serviceConnectionUtils"); + assert.deepEqual(serviceConnectionUtilsWithMocks.getPackagingServiceConnections(serviceConnectionsKey), []); + done(); + }); + + it("getPackagingServiceConnections empty returns empty", (done: MochaDone) => { + let mockTask = { + getDelimitedInput: (key) => { + return []; + } + }; + mockery.registerMock('azure-pipelines-task-lib/task', mockTask); + + let serviceConnectionUtilsWithMocks = require("../serviceConnectionUtils"); + assert.deepEqual(serviceConnectionUtilsWithMocks.getPackagingServiceConnections(serviceConnectionsKey), []); + done(); + }); + + it("getPackagingServiceConnections token good", (done: MochaDone) => { + let mockTask = { + debug: () => {}, + getDelimitedInput: (key) => ["tokenendpoint1"], + getEndpointUrl: (key, optional) => "https://contoso.com/nuget/v3/index.json", + getEndpointAuthorization: (key, optional) => { + parameters: { "apitoken": "sometoken" }, + scheme: "token" + }, + getEndpointAuthorizationScheme: (key, optional): string => "token" + }; + mockery.registerMock('azure-pipelines-task-lib/task', mockTask); + + let serviceConnectionUtilsWithMocks = require("../serviceConnectionUtils"); + assert.deepEqual(serviceConnectionUtilsWithMocks.getPackagingServiceConnections(serviceConnectionsKey), [{ + packageSource: { + uri: "https://contoso.com/nuget/v3/index.json" + }, + authType: ServiceConnectionAuthType.Token, + token: "sometoken" + }]); + done(); + }); + + it("getPackagingServiceConnections token missing apitoken throws", (done: MochaDone) => { + let mockTask = { + debug: () => {}, + getDelimitedInput: (key) => ["tokenendpoint1"], + getEndpointUrl: (key, optional) => "https://contoso.com/nuget/v3/index.json", + getEndpointAuthorization: (key, optional) => { + parameters: { /* missing apitoken */ }, + scheme: "token" + }, + getEndpointAuthorizationScheme: (key, optional): string => "token" + }; + mockery.registerMock('azure-pipelines-task-lib/task', mockTask); + + let serviceConnectionUtilsWithMocks = require("../serviceConnectionUtils"); + assert.throws(() => serviceConnectionUtilsWithMocks.getPackagingServiceConnections(serviceConnectionsKey)); + done(); + }); + + it("getPackagingServiceConnections username/password good", (done: MochaDone) => { + let mockTask = { + debug: () => {}, + getDelimitedInput: (key) => ["tokenendpoint1"], + getEndpointUrl: (key, optional) => "https://contoso.com/nuget/v3/index.json", + getEndpointAuthorization: (key, optional) => { + parameters: { "username": "someusername", "password": "somepassword" }, + scheme: "usernamepassword" + }, + getEndpointAuthorizationScheme: (key, optional): string => "usernamepassword" + }; + mockery.registerMock('azure-pipelines-task-lib/task', mockTask); + + let serviceConnectionUtilsWithMocks = require("../serviceConnectionUtils"); + assert.deepEqual(serviceConnectionUtilsWithMocks.getPackagingServiceConnections(serviceConnectionsKey), [{ + packageSource: { + uri: "https://contoso.com/nuget/v3/index.json" + }, + authType: ServiceConnectionAuthType.UsernamePassword, + username: "someusername", + password: "somepassword" + }]); + done(); + }); + + it("getPackagingServiceConnections username/password missing username throws", (done: MochaDone) => { + let mockTask = { + debug: () => {}, + getDelimitedInput: (key) => ["tokenendpoint1"], + getEndpointUrl: (key, optional) => "https://contoso.com/nuget/v3/index.json", + getEndpointAuthorization: (key, optional) => { + parameters: { /* missing username */ "password": "somepassword" }, + scheme: "usernamepassword" + }, + getEndpointAuthorizationScheme: (key, optional): string => "usernamepassword" + }; + mockery.registerMock('azure-pipelines-task-lib/task', mockTask); + + let serviceConnectionUtilsWithMocks = require("../serviceConnectionUtils"); + assert.throws(() => serviceConnectionUtilsWithMocks.getPackagingServiceConnections(serviceConnectionsKey)); + done(); + }); + + it("getPackagingServiceConnections username/password missing password throws", (done: MochaDone) => { + let mockTask = { + debug: () => {}, + getDelimitedInput: (key) => ["tokenendpoint1"], + getEndpointUrl: (key, optional) => "https://contoso.com/nuget/v3/index.json", + getEndpointAuthorization: (key, optional) => { + parameters: { "username": "someusername" /* missing password */ }, + scheme: "usernamepassword" + }, + getEndpointAuthorizationScheme: (key, optional): string => "usernamepassword" + }; + mockery.registerMock('azure-pipelines-task-lib/task', mockTask); + + let serviceConnectionUtilsWithMocks = require("../serviceConnectionUtils"); + assert.throws(() => serviceConnectionUtilsWithMocks.getPackagingServiceConnections(serviceConnectionsKey)); + done(); + }); +} diff --git a/Tasks/Common/artifacts-common/connectionDataUtils.ts b/Tasks/Common/artifacts-common/connectionDataUtils.ts new file mode 100644 index 000000000000..0440bb1a67d3 --- /dev/null +++ b/Tasks/Common/artifacts-common/connectionDataUtils.ts @@ -0,0 +1,56 @@ +import * as tl from 'azure-pipelines-task-lib/task'; +import * as protocols from './protocols' +import * as api from './webapi'; +import { retryOnException } from './retryUtils'; +import { ConnectOptions } from 'azure-devops-node-api/interfaces/common/VSSInterfaces'; +import { ConnectionData } from 'azure-devops-node-api/interfaces/LocationsInterfaces'; + +/** + * Gets the raw connection data (direct representation of _apis/connectionData) for the service hosting a particular protocol + * @param protocolType The packaging protocol, e.g. 'NuGet' + */ +export async function getConnectionDataForProtocol(protocolType: protocols.ProtocolType) : Promise { + // Retry getting the connection data (which also potentially includes a network call to find the packaging service first), + // since we've previously had reliability issues here. + return await retryOnException(async () => { + // Determine where the Packaging service lives + tl.debug('Finding the URI for the packaging service'); + const accessToken = api.getSystemAccessToken(); + const areaId = protocols.getAreaIdForProtocol(protocolType); + const serviceUri = await getServiceUriFromAreaId(areaId, accessToken); + + // Get _apis/connectionData from the packaging service + const webApi = api.getWebApiWithProxy(serviceUri, accessToken); + const locationApi = await webApi.getLocationsApi(); + tl.debug(`Acquiring connection data for: ${serviceUri}`); + const connectionData = await locationApi.getConnectionData(ConnectOptions.IncludeServices); + tl.debug('Successfully acquired the connection data'); + return connectionData; + }, 3, 1000); +} + +/** + * Gets the URI of the service that hosts an area. + */ +async function getServiceUriFromAreaId(areaId: string, accessToken: string): Promise { + const tfsCollectionUrl = tl.getVariable('System.TeamFoundationCollectionUri'); + const serverType = tl.getVariable('System.ServerType'); + if (!serverType || serverType.toLowerCase() !== 'hosted') { + tl.debug(`Using '${tfsCollectionUrl}' as the service URI since this is on-premises`); + return tfsCollectionUrl; + } + + const webApi = api.getWebApiWithProxy(tfsCollectionUrl, accessToken); + const locationApi = await webApi.getLocationsApi(); + + tl.debug(`Getting URI for area ID ${areaId} from ${tfsCollectionUrl}`); + try { + const serviceUriFromArea = await locationApi.getResourceArea(areaId); + tl.debug(`Acquired the resource area: ${JSON.stringify(serviceUriFromArea)}`); + return serviceUriFromArea.locationUrl; + } catch (error) { + tl.debug(`Failed to obtain the service URI for area ID ${areaId}`); + tl.debug(JSON.stringify(error)); + throw error; + } +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/credentialProviderUtils.ts b/Tasks/Common/artifacts-common/credentialProviderUtils.ts new file mode 100644 index 000000000000..5d4a6117dbed --- /dev/null +++ b/Tasks/Common/artifacts-common/credentialProviderUtils.ts @@ -0,0 +1,195 @@ +import * as fse from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import * as process from 'process'; +import * as tl from 'azure-pipelines-task-lib/task'; +import { getConnectionDataForProtocol } from './connectionDataUtils'; +import { getPackagingAccessMappings } from './packagingAccessMappingUtils'; +import { getSystemAccessToken } from './webapi'; +import { ProtocolType } from './protocols'; +import { ServiceConnection, ServiceConnectionAuthType, UsernamePasswordServiceConnection, TokenServiceConnection } from './serviceConnectionUtils'; +import { retryOnException } from './retryUtils' + +const CRED_PROVIDER_PREFIX_ENVVAR = "VSS_NUGET_URI_PREFIXES"; +const CRED_PROVIDER_ACCESS_TOKEN_ENVVAR = "VSS_NUGET_ACCESSTOKEN"; +const CRED_PROVIDER_EXTERNAL_ENDPOINTS_ENVVAR = "VSS_NUGET_EXTERNAL_FEED_ENDPOINTS"; + +/** + * An entry in VSS_NUGET_EXTERNAL_FEED_ENDPOINTS + */ +interface EndpointCredentials { + endpoint: string; + username?: string; + password: string; +} + +/** + * The object representing VSS_NUGET_EXTERNAL_FEED_ENDPOINTS + */ +interface EndpointCredentialsContainer { + endpointCredentials: EndpointCredentials[]; +} + +/** + * Get the task-provided credential provider plugins directory (containing netcore and netfx folders) + */ +export function getTaskCredProviderPluginsDir(): string { + let taskRootPath: string = path.dirname(path.dirname(__dirname)); + return path.join(taskRootPath, "CredentialProviderV2", "plugins"); +} + + +/** + * Copy the credential provider (netcore and netfx) to the user profile directory + */ +export async function installCredProviderToUserProfile(overwrite: boolean) { + const taskPluginsPir = getTaskCredProviderPluginsDir(); + const userPluginsDir = getUserProfileNuGetPluginsDir(); + + const netCoreSource = path.join(taskPluginsPir, 'netcore', 'CredentialProvider.Microsoft'); + const netCoreDest = path.join(userPluginsDir, 'netcore', 'CredentialProvider.Microsoft'); + console.log(tl.loc("CredProvider_InstallingNetCoreTo", netCoreDest)); + await copyCredProviderFiles(netCoreSource, netCoreDest, overwrite); + console.log(); + + // Only install netfx plugin on Windows + const isWin = process.platform === "win32"; + if (isWin) { + const netFxSource = path.join(taskPluginsPir, 'netfx', 'CredentialProvider.Microsoft'); + const netFxDest = path.join(userPluginsDir, 'netfx', 'CredentialProvider.Microsoft'); + console.log(tl.loc("CredProvider_InstallingNetFxTo", netFxDest)); + await copyCredProviderFiles(netFxSource, netFxDest, overwrite); + console.log(); + } +} + +async function copyCredProviderFiles(source, dest, overwrite) { + // File copy is potentially unreliable, so retry up to 3 times + await retryOnException(async () => { + if (await fse.pathExists(dest) && !overwrite) { + console.log(tl.loc('CredProvider_AlreadyInstalled')); + tl.debug(`Skipping copying from '${source}' to '${dest}' because the destination already exists and overwrite is disabled`); + return; + } + + // We don't want to risk leaving extra old files in the destination if we're overwriting + if (overwrite) { + tl.debug(`Removing '${dest}' before copying from '${source}' since overwrite is enabled`); + try { + await fse.remove(dest); + } catch (ex) { + throw new Error(tl.loc("CredProvider_Error_FailedRemoveDir", dest) + os.EOL + ex) + } + } + + tl.debug(`Copying from '${source}' to '${dest}'`); + try { + await fse.copy(source, dest, { + recursive: true, + overwrite: false, // Intentional - if we're overwriting, + errorOnExist: true // we should have removed the destination already and there shouldn't be any files + }); + } catch (ex) { + throw new Error(tl.loc("CredProvider_Error_FailedCopy", source, dest) + os.EOL + ex) + } + }, 3, 1000); +} + +export function getUserProfileNuGetPluginsDir(): string { + const homeDir = os.homedir(); + return path.join(homeDir, ".nuget", "plugins"); +} + +/** + * Configure the credential provider to provide credentials for feeds within the pipeline's organization, + * as well as for any provided service connections. + */ +export async function configureCredProvider(protocol: ProtocolType, serviceConnections: ServiceConnection[]) { + await configureCredProviderForSameOrganizationFeeds(protocol); + configureCredProviderForServiceConnectionFeeds(serviceConnections); +} + +/** + * Configure the credential provider to provide credentials for feeds within the pipeline's organization, + * using VSS_NUGET_URI_PREFIXES and VSS_NUGET_ACCESSTOKEN variables to do so. + */ +export async function configureCredProviderForSameOrganizationFeeds(protocol: ProtocolType) { + const connectionData = await getConnectionDataForProtocol(protocol); + const packagingAccessMappings = getPackagingAccessMappings(connectionData.locationServiceData); + const accessToken = getSystemAccessToken(); + + // To avoid confusion, only log the public access mapping URIs rather than all of them (e.g. host guid access mapping) + // which we might as well support just in case, yet users are extremely unlikely to ever use. + const allPrefixes: string[] = [...new Set(packagingAccessMappings.map(prefix => prefix.uri))]; + const publicPrefixes: string[] = [...new Set(packagingAccessMappings.filter(prefix => prefix.isPublic).map(prefix => prefix.uri))]; + const identityDisplayName = connectionData.authenticatedUser.customDisplayName || connectionData.authenticatedUser.providerDisplayName; + console.log(tl.loc('CredProvider_SettingUpForOrgFeeds', identityDisplayName)); + publicPrefixes.forEach(publicPrefix => console.log(' ' + publicPrefix)); + console.log(); + + tl.setVariable(CRED_PROVIDER_PREFIX_ENVVAR, allPrefixes.join(";")); + tl.setVariable(CRED_PROVIDER_ACCESS_TOKEN_ENVVAR, accessToken, false /* while this contains secrets, we need the environment variable to be set */); +} + +/** + * Configure the credential provider to provide credentials for service connections, + * using VSS_NUGET_EXTERNAL_FEED_ENDPOINTS to do so. + */ +export function configureCredProviderForServiceConnectionFeeds(serviceConnections: ServiceConnection[]) { + if (serviceConnections && serviceConnections.length) { + console.log(tl.loc('CredProvider_SettingUpForServiceConnections')); + // Ideally we'd also show the service connection name, but the agent doesn't expose it :-( + serviceConnections.map(authInfo => `${authInfo.packageSource.uri}`).forEach(serviceConnectionUri => console.log(' ' + serviceConnectionUri)); + console.log(); + + const externalFeedEndpointsJson = buildExternalFeedEndpointsJson(serviceConnections); + tl.setVariable(CRED_PROVIDER_EXTERNAL_ENDPOINTS_ENVVAR, externalFeedEndpointsJson, false /* while this contains secrets, we need the environment variable to be set */); + } +} + +/** + * Build the JSON for VSS_NUGET_EXTERNAL_FEED_ENDPOINTS + * + * Similar to the older NuGetToolRunner2.buildCredentialJson, + * but fails hard on ApiKey based service connections instead of silently continuing. + */ +export function buildExternalFeedEndpointsJson(serviceConnections: ServiceConnection[]): string { + const endpointCredentialsContainer: EndpointCredentialsContainer = { + endpointCredentials: [] as EndpointCredentials[] + }; + + if (!serviceConnections || !serviceConnections.length) { + return null; + } + + serviceConnections.forEach((serviceConnection: ServiceConnection) => { + switch (serviceConnection.authType) { + case (ServiceConnectionAuthType.UsernamePassword): + const usernamePasswordAuthInfo = serviceConnection as UsernamePasswordServiceConnection; + endpointCredentialsContainer.endpointCredentials.push({ + endpoint: serviceConnection.packageSource.uri, + username: usernamePasswordAuthInfo.username, + password: usernamePasswordAuthInfo.password + }); + tl.debug(`Detected username/password credentials for '${serviceConnection.packageSource.uri}'`); + break; + case (ServiceConnectionAuthType.Token): + const tokenAuthInfo = serviceConnection as TokenServiceConnection; + endpointCredentialsContainer.endpointCredentials.push({ + endpoint: serviceConnection.packageSource.uri, + /* No username provided */ + password: tokenAuthInfo.token + } as EndpointCredentials); + tl.debug(`Detected token credentials for '${serviceConnection.packageSource.uri}'`); + break; + case (ServiceConnectionAuthType.ApiKey): + // e.g. ApiKey based service connections are not supported and cause a hard failure in authentication tasks + const serviceConnectionDisplayText = serviceConnection.packageSource.uri; // Ideally we'd also show the service connection name, but the agent doesn't expose it :-( + throw Error(tl.loc('CredProvider_Error_InvalidServiceConnection_ApiKey', serviceConnectionDisplayText)) + default: + throw Error(tl.loc('CredProvider_Error_InvalidServiceConnection')); + } + }); + + return JSON.stringify(endpointCredentialsContainer); +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/module.json b/Tasks/Common/artifacts-common/module.json new file mode 100644 index 000000000000..fa38f7d778b8 --- /dev/null +++ b/Tasks/Common/artifacts-common/module.json @@ -0,0 +1,15 @@ +{ + "messages": { + "CredProvider_AlreadyInstalled": "The credential provider is already installed. If there are package related authentication failures in later steps, consider setting this task's input 'forceReinstallCredentialProvider' to true to force reinstallation.", + "CredProvider_Error_FailedCopy": "Failed to copy from '%s' to '%s' while installing the credential provider. Ensure that the destination directory is not in use and that the agent account has permission to write to it.", + "CredProvider_Error_FailedRemoveDir": "Failed to remove the directory '%s' while installing the credential provider. Ensure that this directory is not in use and that the agent account has permission to delete this directory.", + "CredProvider_Error_InvalidServiceConnection": "The service connection for '%s' is not valid.", + "CredProvider_Error_InvalidServiceConnection_ApiKey": "The service connection for '%s' is not valid. ApiKey service connections are not supported in this task. Instead, use -ApiKey (NuGet) or --api-key (dotnet) when invoking the tool itself. See the task documentation for more details.", + "CredProvider_InstallingNetCoreTo": "Installing the Azure Artifacts Credential Provider (.NET Core) to '%s'. This credential provider is compatible with dotnet SDK 2.1.400 or later.", + "CredProvider_InstallingNetFxTo": "Installing the Azure Artifacts Credential Provider (.NET Framework) to '%s'. This credential provider is compatible with nuget.exe 4.8.0.5385 or later, and MSBuild 15.8.166.59604 or later.", + "CredProvider_SettingUpForOrgFeeds": "Setting up the credential provider to use the identity '%s' for feeds in your organization/collection starting with:", + "CredProvider_SettingUpForServiceConnections": "Setting up the credential provider for these service connections:", + "ServiceConnections_Error_FailedToParseServiceEndpoint_MissingParameter": "Failed to parse the service endpoint '%s' because it was missing the parameter '%s'", + "ServiceConnections_Error_FailedToParseServiceEndpoint_BadScheme": "Failed to parse the service endpoint '%s' because the auth scheme '%s' was invalid" + } +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/package-lock.json b/Tasks/Common/artifacts-common/package-lock.json new file mode 100644 index 000000000000..ed96db89ba2a --- /dev/null +++ b/Tasks/Common/artifacts-common/package-lock.json @@ -0,0 +1,155 @@ +{ + "name": "artifacts-common", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/fs-extra": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.0.tgz", + "integrity": "sha512-bCtL5v9zdbQW86yexOlXWTEGvLNqWxMFyi7gQA7Gcthbezr2cPSOb8SkESVKA937QD5cIwOFLDFt0MQoXOEr9Q==", + "requires": { + "@types/node": "*" + } + }, + "@types/mocha": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz", + "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==" + }, + "@types/node": { + "version": "10.12.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.9.tgz", + "integrity": "sha512-eajkMXG812/w3w4a1OcBlaTwsFPO5F7fJ/amy+tieQxEMWBlbV1JGSjkFM+zkHNf81Cad+dfIRA+IBkvmvdAeA==" + }, + "azure-devops-node-api": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-8.0.0.tgz", + "integrity": "sha512-QkIzphuE3y/hZVMB6ONN0Dev5r9+CIAiopWulwoYx1Er0kYcsbXsKXKynuLSxsVPocMppbr4YPhTsX2eHY/Mjw==", + "requires": { + "tunnel": "0.0.4", + "typed-rest-client": "1.2.0", + "underscore": "1.8.3" + } + }, + "azure-pipelines-task-lib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-2.8.0.tgz", + "integrity": "sha512-PR8oap9z2j+o455W3PwAfB4SX1p4GdJc9OHQaQV0V+iQS1IBY6dVgcNSQMkHAXb0V1bbuLOFBLanXPe5eSgGTQ==", + "requires": { + "minimatch": "3.0.4", + "mockery": "^1.7.0", + "q": "^1.1.2", + "semver": "^5.1.0", + "shelljs": "^0.3.0", + "uuid": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mockery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mockery/-/mockery-1.7.0.tgz", + "integrity": "sha1-9O3g2HUMHJcnwnLqLGBiniyaHE8=" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=" + }, + "tunnel": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", + "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=" + }, + "typed-rest-client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.2.0.tgz", + "integrity": "sha512-FrUshzZ1yxH8YwGR29PWWnfksLEILbWJydU7zfIRkyH7kAEzB62uMAl2WY6EyolWpLpVHeJGgQm45/MaruaHpw==", + "requires": { + "tunnel": "0.0.4", + "underscore": "1.8.3" + } + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } +} diff --git a/Tasks/Common/artifacts-common/package.json b/Tasks/Common/artifacts-common/package.json new file mode 100644 index 000000000000..e55127a9885f --- /dev/null +++ b/Tasks/Common/artifacts-common/package.json @@ -0,0 +1,23 @@ +{ + "name": "artifacts-common", + "version": "0.1.0", + "description": "Azure Artifacts common code (for new authentication tasks)", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Microsoft Corporation", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/azure-pipelines-tasks" + }, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "8.0.0", + "@types/node": "10.12.9", + "@types/mocha": "5.2.6", + "azure-devops-node-api": "8.0.0", + "azure-pipelines-task-lib": "2.8.0", + "fs-extra": "8.1.0", + "semver": "6.3.0" + } +} diff --git a/Tasks/Common/artifacts-common/packagingAccessMappingUtils.ts b/Tasks/Common/artifacts-common/packagingAccessMappingUtils.ts new file mode 100644 index 000000000000..1fc55537b165 --- /dev/null +++ b/Tasks/Common/artifacts-common/packagingAccessMappingUtils.ts @@ -0,0 +1,65 @@ +import { AccessMapping, LocationServiceData } from "azure-devops-node-api/interfaces/LocationsInterfaces"; + +/** + * Represents information about an URI used to access a service (e.g. https://pkgs.dev.azure.com/contoso/) + * These uri prefixes are used e.g. by credential providers to determine which URIs to provide credentials for. + * + * Essentially a higher-level AccessMapping that provides a normalized URI (handles virtualdirectory and adds trailing slash) and information about its intended uses. + */ +export interface PackagingAccessMapping { + /** + * Uri such as https://pkgs.dev.azure.com/contoso/, https://contoso.pkgs.visualstudio.com/, or https://pkgs.visualstudio.com/Aeab00668-a6f3-4174-940b-5107d345e830/ + * Always has a trailing slash. + */ + uri: string; + + /** + * True if a well-known and publicly used access mapping. + * False otherwise, e.g. a HostGuidAccessMapping like https://pkgs.visualstudio.com/Aeab00668-a6f3-4174-940b-5107d345e830 + */ + isPublic: boolean; + + /** + * True if this is the default access mapping, i.e. based on the user's preference for dev.azure.com vs visualstudio.com URLs. + * False otherwise. + */ + isDefault: boolean; +} + +/** + * Converts location service data into higher-level "uri prefixes, e.g. "https://pkgs.dev.azure.com/contoso/" + * These uri prefixes are used e.g. by credential providers to determine which URIs to provide credentials for. + * + * To use this API, first get connectionData, then pass connectionData.locationServiceData + */ +export function getPackagingAccessMappings(locationServiceData: LocationServiceData) : PackagingAccessMapping[] { + // Both forms of the public access mapping, e.g. https://pkgs.dev.azure.com/{organization}/ and https://{organization}.pkgs.visualstudio.com/ + const commonAccessMappings = ['CodexAccessMapping', 'VstsAccessMapping']; + + return locationServiceData.accessMappings.map(accessMapping => { + const isDefaultAccessMapping = accessMapping.moniker === locationServiceData.defaultAccessMappingMoniker; + const isCommonAccessMapping = commonAccessMappings.indexOf(accessMapping.moniker) > -1; + return { + uri: toNormalizedAccessUri(accessMapping), + isPublic: isDefaultAccessMapping || isCommonAccessMapping, + isDefault: isDefaultAccessMapping + } + }); +} + +/** + * Converts an access mapping to a packaging URI prefix (e.g. "https://pkgs.dev.azure.com/contoso/"), + * ensuring trailing slashes and combining access points and virtual directories (e.g. for HostGuidAccessMappings). + */ +function toNormalizedAccessUri(accessMapping: AccessMapping): string { + // We always ensure a trailing slash, since we use these access points in prefix comparisons + if (!accessMapping.virtualDirectory) { + return ensureTrailingSlash(accessMapping.accessPoint); + } + + return ensureTrailingSlash(ensureTrailingSlash(accessMapping.accessPoint) + accessMapping.virtualDirectory); +} + +function ensureTrailingSlash(uri: string) { + return uri.endsWith("/") ? uri : uri + "/"; +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/protocols.ts b/Tasks/Common/artifacts-common/protocols.ts new file mode 100644 index 000000000000..c3a5cad30a3c --- /dev/null +++ b/Tasks/Common/artifacts-common/protocols.ts @@ -0,0 +1,20 @@ +export enum ProtocolType { + NuGet, + Maven, + Npm, + PyPi +} + +export function getAreaIdForProtocol(protocolType: ProtocolType): string { + switch (protocolType) { + case ProtocolType.Maven: + return '6F7F8C07-FF36-473C-BCF3-BD6CC9B6C066'; + case ProtocolType.Npm: + return '4C83CFC1-F33A-477E-A789-29D38FFCA52E'; + case ProtocolType.PyPi: + return '92F0314B-06C5-46E0-ABE7-15FD9D13276A'; + default: + case ProtocolType.NuGet: + return 'B3BE7473-68EA-4A81-BFC7-9530BAAA19AD'; + } +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/retryUtils.ts b/Tasks/Common/artifacts-common/retryUtils.ts new file mode 100644 index 000000000000..32eda0960bf6 --- /dev/null +++ b/Tasks/Common/artifacts-common/retryUtils.ts @@ -0,0 +1,22 @@ +import * as tl from 'azure-pipelines-task-lib/task'; + +export async function retryOnException(action: () => Promise, maxTries: number, retryIntervalInMilliseconds: number): Promise { + while (true) { + try { + return await action(); + } catch (error) { + maxTries--; + if (maxTries < 1) { + tl.debug(`Exhausted retry attempts`); + throw error; + } + tl.debug(`Attempt failed. Number of tries left: ${maxTries}`); + tl.debug(JSON.stringify(error)); + await delay(retryIntervalInMilliseconds); + } + } +} + +function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/serviceConnectionUtils.ts b/Tasks/Common/artifacts-common/serviceConnectionUtils.ts new file mode 100644 index 000000000000..d1206cf556b5 --- /dev/null +++ b/Tasks/Common/artifacts-common/serviceConnectionUtils.ts @@ -0,0 +1,128 @@ +import * as tl from "azure-pipelines-task-lib/task"; + +export interface IExternalPackageSource { + /** + * The protocol-specific URI used to access the package source + */ + uri: string; +} + +export abstract class ServiceConnection +{ + constructor( + public packageSource: IExternalPackageSource, + public authType: ServiceConnectionAuthType) { + } +} + +export class TokenServiceConnection extends ServiceConnection +{ + constructor( + public packageSource: IExternalPackageSource, + public token: string) + { + super(packageSource, ServiceConnectionAuthType.Token); + } +} + +export class UsernamePasswordServiceConnection extends ServiceConnection +{ + constructor( + public packageSource: IExternalPackageSource, + public username: string, + public password: string) + { + super(packageSource, ServiceConnectionAuthType.UsernamePassword); + } +} + +export class ApiKeyServiceConnection extends ServiceConnection +{ + constructor( + public packageSource: IExternalPackageSource, + public apiKey: string) + { + super(packageSource, ServiceConnectionAuthType.ApiKey); + } +} + +export enum ServiceConnectionAuthType +{ + Token, + UsernamePassword, + ApiKey +} + +/** + * Parses service connections / service endpoints from a task input into strongly typed objects containing the URI and credentials. + * + * @param endpointsInputName The name of the task input containing the service connections endpoints + */ +export function getPackagingServiceConnections(endpointsInputName: string): ServiceConnection[] +{ + let endpointNames = tl.getDelimitedInput(endpointsInputName, ','); + + if (!endpointNames || endpointNames.length === 0) + { + return []; + } + + let serviceConnections: ServiceConnection[] = []; + endpointNames.forEach((endpointName: string) => { + let uri = tl.getEndpointUrl(endpointName, false); + let endpointAuth = tl.getEndpointAuthorization(endpointName, true); + let endpointAuthScheme = tl.getEndpointAuthorizationScheme(endpointName, true).toLowerCase(); + + switch (endpointAuthScheme) { + case "token": + if (!("apitoken" in endpointAuth.parameters)) { + throw Error(tl.loc("ServiceConnections_Error_FailedToParseServiceEndpoint_MissingParameter", uri, "apitoken")); + } + + let token = endpointAuth.parameters["apitoken"]; + tl.debug("Found token service connection for package source " + uri); + serviceConnections.push(new TokenServiceConnection( + { + uri: uri + }, + token)); + break; + case "usernamepassword": + if (!("username" in endpointAuth.parameters)) { + throw Error(tl.loc("ServiceConnections_Error_FailedToParseServiceEndpoint_MissingParameter", uri, "username")); + } + + if (!("password" in endpointAuth.parameters)) { + throw Error(tl.loc("ServiceConnections_Error_FailedToParseServiceEndpoint_MissingParameter", uri, "password")); + } + + let username = endpointAuth.parameters["username"]; + let password = endpointAuth.parameters["password"]; + tl.debug("Found username/password service connection for package source " + uri); + serviceConnections.push(new UsernamePasswordServiceConnection( + { + uri: uri + }, + username, + password)); + break; + case "none": // We only support this for nuget today. npm and python tasks do not use this endpoint auth scheme. + if ("nugetkey" in endpointAuth.parameters) { + throw Error(tl.loc("ServiceConnections_Error_FailedToParseServiceEndpoint_MissingParameter", uri, "nugetkey")); + } + + let apiKey = endpointAuth.parameters["nugetkey"]; + tl.debug("Found nuget apikey service connection for package source " + uri); + serviceConnections.push(new ApiKeyServiceConnection( + { + uri: uri + }, + apiKey)); + break; + default: + throw Error(tl.loc("ServiceConnections_Error_FailedToParseServiceEndpoint_BadScheme", uri, endpointAuthScheme)); + } + }); + + return serviceConnections; +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/telemetry.ts b/Tasks/Common/artifacts-common/telemetry.ts new file mode 100644 index 000000000000..5c397feb5bf5 --- /dev/null +++ b/Tasks/Common/artifacts-common/telemetry.ts @@ -0,0 +1,47 @@ +import * as tl from 'azure-pipelines-task-lib/task'; +import * as semver from 'semver'; + +/** + * Utility function to log telemetry. + * @param feature The task/feature name for this telemetry + * @param telem A JSON object containing a dictionary of variables that will be appended to + * common system vars and loggged. + */ +export function emitTelemetry(area: string, feature: string, taskSpecificTelemetry: any) { + try { + let agentVersion = tl.getVariable('Agent.Version'); + if (semver.gte(agentVersion, '2.120.0')) { + // Common Telemetry VARs that will be concatenated with the supplied telem object. + let commonTelem = { + 'SYSTEM_TASKINSTANCEID': tl.getVariable('SYSTEM_TASKINSTANCEID'), + 'SYSTEM_JOBID': tl.getVariable('SYSTEM_JOBID'), + 'SYSTEM_PLANID': tl.getVariable('SYSTEM_PLANID'), + 'SYSTEM_COLLECTIONID': tl.getVariable('SYSTEM_COLLECTIONID'), + 'SYSTEM_PULLREQUEST_ISFORK': tl.getVariable('SYSTEM_PULLREQUEST_ISFORK'), + 'AGENT_ID': tl.getVariable('AGENT_ID'), + 'AGENT_MACHINENAME': tl.getVariable('AGENT_MACHINENAME'), + 'AGENT_NAME': tl.getVariable('AGENT_NAME'), + 'AGENT_JOBSTATUS': tl.getVariable('AGENT_JOBSTATUS'), + 'AGENT_OS': tl.getVariable('AGENT_OS'), + 'AGENT_OSARCHITECTURE': tl.getVariable('AGENT_OSARCHITECTURE'), + 'AGENT_VERSION': tl.getVariable('AGENT_VERSION'), + 'BUILD_BUILDID': tl.getVariable('BUILD_BUILDID'), + 'BUILD_BUILDNUMBER': tl.getVariable('BUILD_BUILDNUMBER'), + 'BUILD_BUILDURI': tl.getVariable('BUILD_BUILDURI'), + 'BUILD_CONTAINERID': tl.getVariable('BUILD_CONTAINERID'), + 'BUILD_DEFINITIONNAME': tl.getVariable('BUILD_DEFINITIONNAME'), + 'BUILD_DEFINITIONVERSION': tl.getVariable('BUILD_DEFINITIONVERSION'), + 'BUILD_REASON': tl.getVariable('BUILD_REASON') + }; + let copy = Object.assign(commonTelem, taskSpecificTelemetry); + console.log("##vso[telemetry.publish area=%s;feature=%s]%s", + area, + feature, + JSON.stringify(copy)); + } else { + tl.debug(`Agent version of ( ${agentVersion} ) does not meet minimum requirements for telemetry`); + } + } catch (err) { + tl.debug(`Unable to log telemetry. Err:( ${err} )`); + } +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/tsconfig.json b/Tasks/Common/artifacts-common/tsconfig.json new file mode 100644 index 000000000000..e11a18b8de6a --- /dev/null +++ b/Tasks/Common/artifacts-common/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "declaration": true, + "noImplicitAny": false, + "sourceMap": false + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/Tasks/Common/artifacts-common/webapi.ts b/Tasks/Common/artifacts-common/webapi.ts new file mode 100644 index 000000000000..70a2c5c5d545 --- /dev/null +++ b/Tasks/Common/artifacts-common/webapi.ts @@ -0,0 +1,25 @@ +import * as api from 'azure-devops-node-api'; +import { IRequestOptions } from 'azure-devops-node-api/interfaces/common/VsoBaseInterfaces'; +import * as tl from 'azure-pipelines-task-lib/task'; + +export function getWebApiWithProxy(serviceUri: string, accessToken: string): api.WebApi { + const credentialHandler = api.getBasicHandler('vsts', accessToken); + const options: IRequestOptions = { + proxy: tl.getHttpProxyConfiguration(serviceUri), + allowRetries: true, + maxRetries: 5 + }; + + return new api.WebApi(serviceUri, credentialHandler, options); +} + +export function getSystemAccessToken(): string { + tl.debug('Getting credentials for local feeds'); + const auth = tl.getEndpointAuthorization('SYSTEMVSSCONNECTION', false); + if (auth.scheme === 'OAuth') { + tl.debug('Got auth token'); + return auth.parameters['AccessToken']; + } else { + tl.warning('Could not determine credentials to use'); + } +} \ No newline at end of file diff --git a/Tasks/NuGetAuthenticateV0/Strings/resources.resjson/en-US/resources.resjson b/Tasks/NuGetAuthenticateV0/Strings/resources.resjson/en-US/resources.resjson new file mode 100644 index 000000000000..bc916efad3d3 --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/Strings/resources.resjson/en-US/resources.resjson @@ -0,0 +1,10 @@ +{ + "loc.friendlyName": "NuGet authenticate", + "loc.helpMarkDown": "[Learn more about this task](https://aka.ms/NuGetAuthenticateTask)", + "loc.description": "Configure NuGet tools to authenticate with Azure Artifacts and other NuGet repositories. Requires NuGet >= 4.8.5385, dotnet >= 2.1.400, or MSBuild >= 15.8.166.59604", + "loc.instanceNameFormat": "NuGet Authenticate", + "loc.input.label.nuGetServiceConnections": "Service connection credentials for feeds outside this organization", + "loc.input.help.nuGetServiceConnections": "Comma-separated list of NuGet service connection names for feeds outside this organization/collection. For feeds in this organization/collection, leave this blank; the build’s credentials are used automatically.", + "loc.input.label.forceReinstallCredentialProvider": "Reinstall the credential provider even if already installed", + "loc.input.help.forceReinstallCredentialProvider": "If the credential provider is already installed in the user profile, determines if it is overwritten with the task-provided credential provider. This may upgrade (or potentially downgrade) the credential provider." +} \ No newline at end of file diff --git a/Tasks/NuGetAuthenticateV0/icon.png b/Tasks/NuGetAuthenticateV0/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4140f9bd9693f66a5521dd4f77a87d28a01b3b13 GIT binary patch literal 1621 zcmV-b2CDgqP)(9@`Ml40EBlx@ooKQ?7_**hnFNc05P1cJp0A5oL`kKsIrA$PZvD9Pg`5}XS0z@ z0idT#GVGM>SwE&q4l#2xfaL!Uup(^iZZq)cc-Gbc!0Qp**XA1?2?kGI=r}kveHSr3 zD4F_-677y+&wS#BE-X{1(**p;gwOh-p#yV$=s+^LbRtLJ>^PKKS#l(8=?N!#V!p@L zGzSL&VBb`3rDA^fNJicIR~|T1W++uX%*-`GiRkPXfC$h9p~Ek*qCw%x2KtuB^ITul zJblg|00x=;zD!}AbW}hY80ZZKlK?Q3oZc{5rZ4;BvN2@=idFT<*}x1JfSJgseh>&m z1QI~257-#PWY|NS*0wYqJO|)rL&@CLBZbO=@5WTXQUpgz#3#y_O4%q`5=zwuztDsr z_`MpU5igdsn$WZ=jHv@#mxx5nFK@Uh5*c*`U}mv1p84(5Q*u?oQAnn39Q`|oc2h&T zq>!=XMT5^ZuOZNLRU;xHFF-)VC-CqR(SJvK(><;L`zEI!>5n@*{zxk%rz_C1Hnwf< zKv&oR1NiD_7B7DvN8VNoh9Cm2?G7Q@?12y(dZUD!!}>bm=A3u%#Ib1w+g2DGDPa5k zm!T_cRD}RMaCH!`-_g3LEHDE{Ph??P3eu4{oRUx^A9V%zr@*&Yq{2wNfc2M~Xl>AI zd>-xzLKk&Q$ki&6F*05RD@NXCj1+Kp4ZuJm(U>YJla)duX`{*K$_TG6;MErg004=U z4W%SlF)}4?uTj^92o)U#M>0wk#{RJq+zS5CL>Yxe)WD3D?5ei1C9H~SaJ`;V3Y7{2 z4E#A|;qAexSskA)OT6-Ze9^%H01T815>CE4&}tv^)V$?aKm4mnFVF zmc^O8^Pl2d1qGQ`P^JJB*8`}{`QT^~gQLa&6tpVIC}tUUQGpdhNd|LW0Gu1Qi?J0^ zFP^`#8GVI4poC{qPn7q>eK)c1l36n14;r;MZvKc z9qskYXs&g~yyx`=frJZyqf{n90-7Y0a}uloRs`m1>qv=NU0QJ#J`F390Rp^U4MGsS zI^}DwxxS?(7xIXr&N|2;SVe|iR;XAE>7Zsr{2sR!3~+e748K={N3TADfFUMa^Sm@j zdo`WV(c+(1^hCnKzTt%psE>Y5qdjUs2m!;Rp|g>8x&lP}^igL>M|W2PG;RKBeCf+^ zlq(k>TmvTy`1oK3S9S(KLZBz6!>@}^T>(a;!5w`abi6TOV9l!Lc{}3ZXbD^18%DC= z{#R|+@jUK({}fhr2Vt5X_zexc9b#8+EV#F>^iPxN>kcQieS1cgZzNv8FNf3f7F^iW z@bua+Zs`i3$*Zl*Wzqjv9;Ei8%$(3A4%c! zJ!8sWC`p_c&SQMCh+N)*BQKtET_ZFGbhO9(Sl-zHuUEh$2pgA)bayj0t&N0+fCY@3 zp^4PBlNIfQUna!$Co>9Co!wJh-DWfI1{covnMpVEPM`_Kiip6P81D|6{M5Q|_|!Sy z`2fy})BZ&Eo+9Is@jPxVIn-uL_~!V`IzvP-G|rnko#`<72UDX@dSk)8bG82k23VUh TEYhhc00000NkvXXu0mjfEIQ~M literal 0 HcmV?d00001 diff --git a/Tasks/NuGetAuthenticateV0/icon.svg b/Tasks/NuGetAuthenticateV0/icon.svg new file mode 100644 index 000000000000..76379b002043 --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/Tasks/NuGetAuthenticateV0/main.ts b/Tasks/NuGetAuthenticateV0/main.ts new file mode 100644 index 000000000000..584dacd7832c --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/main.ts @@ -0,0 +1,30 @@ +import * as path from 'path'; +import * as tl from 'azure-pipelines-task-lib/task'; +import { installCredProviderToUserProfile, configureCredProvider } from 'artifacts-common/credentialProviderUtils' +import { ProtocolType } from 'artifacts-common/protocols'; +import { getPackagingServiceConnections } from 'artifacts-common/serviceConnectionUtils' +import { emitTelemetry } from 'artifacts-common/telemetry' + +async function main(): Promise { + let forceReinstallCredentialProvider = null; + try { + tl.setResourcePath(path.join(__dirname, 'task.json')); + tl.setResourcePath(path.join(__dirname, 'node_modules/artifacts-common/module.json')); + + // Install the credential provider + forceReinstallCredentialProvider = tl.getBoolInput("forceReinstallCredentialProvider", false); + await installCredProviderToUserProfile(forceReinstallCredentialProvider); + + // Configure the credential provider for both same-organization feeds and service connections + const serviceConnections = getPackagingServiceConnections('nuGetServiceConnections'); + await configureCredProvider(ProtocolType.NuGet, serviceConnections); + } catch (error) { + tl.setResult(tl.TaskResult.Failed, error); + } finally { + emitTelemetry("Packaging", "NuGetAuthenticate", { + 'NuGetAuthenticate.ForceReinstallCredentialProvider': forceReinstallCredentialProvider + }); + } +} + +main(); \ No newline at end of file diff --git a/Tasks/NuGetAuthenticateV0/make.json b/Tasks/NuGetAuthenticateV0/make.json new file mode 100644 index 000000000000..49eb1e5e3de9 --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/make.json @@ -0,0 +1,25 @@ +{ + "common": [ + { + "module": "../Common/artifacts-common", + "type": "node", + "compile": true + } + ], + "rm": [ + { + "items": [ + "node_modules/artifacts-common/node_modules/azure-pipelines-task-lib" + ], + "options": "-Rf" + } + ], + "externals": { + "archivePackages": [ + { + "url": "https://vstsagenttools.blob.core.windows.net/tools/NuGetCredProvider/0.1.18/c.zip", + "dest": "./CredentialProviderV2/" + } + ] + } +} \ No newline at end of file diff --git a/Tasks/NuGetAuthenticateV0/package-lock.json b/Tasks/NuGetAuthenticateV0/package-lock.json new file mode 100644 index 000000000000..721233e1f24c --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/package-lock.json @@ -0,0 +1,167 @@ +{ + "name": "nugetauthenticate", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/fs-extra": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.0.0.tgz", + "integrity": "sha512-bCtL5v9zdbQW86yexOlXWTEGvLNqWxMFyi7gQA7Gcthbezr2cPSOb8SkESVKA937QD5cIwOFLDFt0MQoXOEr9Q==", + "requires": { + "@types/node": "*" + } + }, + "@types/mocha": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz", + "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==" + }, + "@types/node": { + "version": "10.12.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.9.tgz", + "integrity": "sha512-eajkMXG812/w3w4a1OcBlaTwsFPO5F7fJ/amy+tieQxEMWBlbV1JGSjkFM+zkHNf81Cad+dfIRA+IBkvmvdAeA==" + }, + "artifacts-common": { + "version": "file:../../_build/Tasks/Common/artifacts-common-0.1.0.tgz", + "requires": { + "@types/fs-extra": "8.0.0", + "@types/mocha": "5.2.6", + "@types/node": "10.12.9", + "azure-devops-node-api": "8.0.0", + "azure-pipelines-task-lib": "2.8.0", + "fs-extra": "8.1.0", + "semver": "6.3.0" + } + }, + "azure-devops-node-api": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-8.0.0.tgz", + "integrity": "sha512-QkIzphuE3y/hZVMB6ONN0Dev5r9+CIAiopWulwoYx1Er0kYcsbXsKXKynuLSxsVPocMppbr4YPhTsX2eHY/Mjw==", + "requires": { + "tunnel": "0.0.4", + "typed-rest-client": "1.2.0", + "underscore": "1.8.3" + } + }, + "azure-pipelines-task-lib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-2.8.0.tgz", + "integrity": "sha512-PR8oap9z2j+o455W3PwAfB4SX1p4GdJc9OHQaQV0V+iQS1IBY6dVgcNSQMkHAXb0V1bbuLOFBLanXPe5eSgGTQ==", + "requires": { + "minimatch": "3.0.4", + "mockery": "^1.7.0", + "q": "^1.1.2", + "semver": "^5.1.0", + "shelljs": "^0.3.0", + "uuid": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mockery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mockery/-/mockery-1.7.0.tgz", + "integrity": "sha1-9O3g2HUMHJcnwnLqLGBiniyaHE8=" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=" + }, + "tunnel": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", + "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=" + }, + "typed-rest-client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.2.0.tgz", + "integrity": "sha512-FrUshzZ1yxH8YwGR29PWWnfksLEILbWJydU7zfIRkyH7kAEzB62uMAl2WY6EyolWpLpVHeJGgQm45/MaruaHpw==", + "requires": { + "tunnel": "0.0.4", + "underscore": "1.8.3" + } + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } +} \ No newline at end of file diff --git a/Tasks/NuGetAuthenticateV0/package.json b/Tasks/NuGetAuthenticateV0/package.json new file mode 100644 index 000000000000..93032cd3f73a --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/package.json @@ -0,0 +1,28 @@ +{ + "name": "nugetauthenticate", + "version": "0.1.0", + "description": "NuGet Authenticate", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/azure-pipelines-tasks.git" + }, + "keywords": [ + "Azure", + "Pipelines", + "Tasks", + "NuGet" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Microsoft/azure-pipelines-tasks/issues" + }, + "homepage": "https://github.com/Microsoft/azure-pipelines-tasks#readme", + "dependencies": { + "artifacts-common": "file:../../_build/Tasks/Common/artifacts-common-0.1.0.tgz" + } +} diff --git a/Tasks/NuGetAuthenticateV0/task.json b/Tasks/NuGetAuthenticateV0/task.json new file mode 100644 index 000000000000..c70d632a4bbe --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/task.json @@ -0,0 +1,48 @@ +{ + "id": "f5fd8599-ccfa-4d6e-b965-4d14bed7097b", + "name": "NuGetAuthenticate", + "friendlyName": "NuGet authenticate", + "description": "Configure NuGet tools to authenticate with Azure Artifacts and other NuGet repositories. Requires NuGet >= 4.8.5385, dotnet >= 2.1.400, or MSBuild >= 15.8.166.59604", + "author": "Microsoft Corporation", + "helpUrl": "https://aka.ms/NuGetAuthenticateTask", + "helpMarkDown": "[Learn more about this task](https://aka.ms/NuGetAuthenticateTask)", + "category": "Package", + "runsOn": [ + "Agent", + "DeploymentGroup" + ], + "version": { + "Major": 0, + "Minor": 156, + "Patch": 0 + }, + "minimumAgentVersion": "2.120.0", + "instanceNameFormat": "NuGet Authenticate", + "inputs": [ + { + "name": "nuGetServiceConnections", + "type": "connectedService:ExternalNuGetFeed", + "label": "Service connection credentials for feeds outside this organization", + "required": false, + "helpMarkDown": "Comma-separated list of NuGet service connection names for feeds outside this organization/collection. For feeds in this organization/collection, leave this blank; the build’s credentials are used automatically.", + "properties": { + "EditableOptions": "False", + "MultiSelectFlatList": "True" + } + }, + { + "name": "forceReinstallCredentialProvider", + "type": "boolean", + "label": "Reinstall the credential provider even if already installed", + "defaultValue": "false", + "helpMarkDown": "If the credential provider is already installed in the user profile, determines if it is overwritten with the task-provided credential provider. This may upgrade (or potentially downgrade) the credential provider." + } + ], + "execution": { + "Node": { + "target": "main.js" + } + }, + "messages": { + } +} diff --git a/Tasks/NuGetAuthenticateV0/task.loc.json b/Tasks/NuGetAuthenticateV0/task.loc.json new file mode 100644 index 000000000000..c7789dc9aacc --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/task.loc.json @@ -0,0 +1,47 @@ +{ + "id": "f5fd8599-ccfa-4d6e-b965-4d14bed7097b", + "name": "NuGetAuthenticate", + "friendlyName": "ms-resource:loc.friendlyName", + "description": "ms-resource:loc.description", + "author": "Microsoft Corporation", + "helpUrl": "https://aka.ms/NuGetAuthenticateTask", + "helpMarkDown": "ms-resource:loc.helpMarkDown", + "category": "Package", + "runsOn": [ + "Agent", + "DeploymentGroup" + ], + "version": { + "Major": 0, + "Minor": 156, + "Patch": 0 + }, + "minimumAgentVersion": "2.120.0", + "instanceNameFormat": "ms-resource:loc.instanceNameFormat", + "inputs": [ + { + "name": "nuGetServiceConnections", + "type": "connectedService:ExternalNuGetFeed", + "label": "ms-resource:loc.input.label.nuGetServiceConnections", + "required": false, + "helpMarkDown": "ms-resource:loc.input.help.nuGetServiceConnections", + "properties": { + "EditableOptions": "False", + "MultiSelectFlatList": "True" + } + }, + { + "name": "forceReinstallCredentialProvider", + "type": "boolean", + "label": "ms-resource:loc.input.label.forceReinstallCredentialProvider", + "defaultValue": "false", + "helpMarkDown": "ms-resource:loc.input.help.forceReinstallCredentialProvider" + } + ], + "execution": { + "Node": { + "target": "main.js" + } + }, + "messages": {} +} \ No newline at end of file diff --git a/Tasks/NuGetAuthenticateV0/tsconfig.json b/Tasks/NuGetAuthenticateV0/tsconfig.json new file mode 100644 index 000000000000..0438b79f69ac --- /dev/null +++ b/Tasks/NuGetAuthenticateV0/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs" + } +} \ No newline at end of file diff --git a/make-options.json b/make-options.json index 21e3f137acac..f88613c2e7a7 100644 --- a/make-options.json +++ b/make-options.json @@ -104,6 +104,7 @@ "NodeToolV0", "NpmV1", "NpmAuthenticateV0", + "NuGetAuthenticateV0", "NuGetV0", "NuGetCommandV2", "NuGetPackagerV0",