Skip to content

Commit

Permalink
feat(private npm registry): Enable option to use private npm registry…
Browse files Browse the repository at this point in the history
… Url in a11y ado extension (#2162)

#### Details

<!-- Usually a sentence or two describing what the PR changes -->
Added two new optional parameter `npmRegistryUrl` and
`npmRegistryCredential` to enable option to use private npm registry Url
in a11y ado extension.
If npmRegistryUrl is not set it will use https://registry.yarnpkg.com by
default

##### Motivation

<!-- This can be as simple as "addresses issue #123" -->
[Feature
2207295](https://dev.azure.com/mseng/1ES/_workitems/edit/2207295):
Enable option to use private npm registry Url in a11y ado extension

##### Context

<!-- Are there any parts that you've intentionally left out-of-scope for
a later PR to handle? -->

<!-- Were there any alternative approaches you considered? What
tradeoffs did you consider? -->

#### Pull request checklist
<!-- If a checklist item is not applicable to this change, write "n/a"
in the checkbox -->
- [ ] Addresses an existing issue: Fixes #0000
- [x] Added relevant unit test for your changes. (`yarn test`)
- [x] Verified code coverage for the changes made. Check coverage report
at: `<rootDir>/test-results/unit/coverage`
- [x] Ran precheckin (`yarn precheckin`)

---------

Co-authored-by: Vikash Yadav <[email protected]>
  • Loading branch information
v-sharmachir and v-viyada authored Sep 3, 2024
1 parent 0ff1498 commit e31b238
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/ado-extension-inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ or
- `snapshot` (boolean). Save snapshots of scanned pages as artifacts. These snapshots will show you exactly what the scanner sees when scanning the page. This requires `uploadOutputArtifact` to not be set to `false`.

- `chromePath` (string). Path to Chrome executable. By default, the task downloads a version of Chrome that is tested to work with this task.

- `npmRegistryUrl` (string). Default: `https://registry.yarnpkg.com`. NPM registry Url to install the runtime dependencies.

- `npmRegistryCredential` (connectedService:externalnpmregistry). Credentials to use for external registries to install the runtime dependencies. For registries in this organization/collection, leave this blank; the build’s credentials are used automatically.
35 changes: 35 additions & 0 deletions packages/ado-extension/src/install-runtime-dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { execFileSync } from 'child_process';
import { argv } from 'process';
import { readdirSync } from 'fs';
import { join } from 'path';
import * as adoTask from 'azure-pipelines-task-lib/task';
import * as npmRegistryUtil from './npm-registry-util';

export function installRuntimeDependencies(): void {
console.log('##[group]Installing runtime dependencies');
Expand All @@ -28,6 +30,39 @@ export function installRuntimeDependencies(): void {
console.log(`##[debug]Using node from ${nodePath}`);
console.log(`##[debug]Using bundled yarn from ${yarnPath}`);

const registryUrl: string = adoTask.getInput('npmRegistryUrl') || 'https://registry.yarnpkg.com';

console.log(`Using registry URL: ${registryUrl}`);
// Set the Yarn registry URL
execFileSync(nodePath, [yarnPath, 'config', 'set', 'npmRegistryServer', registryUrl], {
stdio: 'inherit',
cwd: __dirname,
});

if (registryUrl != 'https://registry.yarnpkg.com') {
const serviceConnectionName: string | undefined = adoTask.getInput('npmRegistryCredential');

if (!serviceConnectionName) {
execFileSync(nodePath, [yarnPath, 'config', 'set', 'npmAuthToken', npmRegistryUtil.getSystemAccessToken()], {
stdio: 'inherit',
cwd: __dirname,
});
} else {
execFileSync(
nodePath,
[yarnPath, 'config', 'set', 'npmAuthIdent', npmRegistryUtil.getTokenFromServiceConnection(serviceConnectionName ?? '')],
{
stdio: 'inherit',
cwd: __dirname,
},
);
}
execFileSync(nodePath, [yarnPath, 'config', 'set', 'npmAlwaysAuth', 'true'], {
stdio: 'inherit',
cwd: __dirname,
});
}

execFileSync(nodePath, [yarnPath, 'install', '--immutable'], {
stdio: 'inherit',
cwd: __dirname,
Expand Down
74 changes: 74 additions & 0 deletions packages/ado-extension/src/npm-registry-util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as adoTask from 'azure-pipelines-task-lib/task';
import * as NpmRegistryUtil from './npm-registry-util';
import { Mock } from 'typemoq';

describe('NpmRegistryUtil', () => {
const authenticationMock = Mock.ofType<adoTask.EndpointAuthorization>();

describe('Improper Authentications', () => {
it('should not get system access token authentication when scheme is not OAuth', () => {
const auth = jest.spyOn(adoTask, 'getEndpointAuthorization');
auth.mockReturnValue(authenticationMock.object);
const accessToken = NpmRegistryUtil.getSystemAccessToken();
expect(accessToken).toBeFalsy();
});

it('should not get npmAuthIdent from service connection when service connection does not exist', () => {
const auth = jest.spyOn(adoTask, 'getEndpointAuthorization');
auth.mockReturnValue(undefined);
expect(() => NpmRegistryUtil.getTokenFromServiceConnection('serviceConnectionName')).toThrowError(
'Could not find the service connection',
);
});

it('should not get npmAuthIdent from service connection when service connection scheme is other than Token or UsernamePassword', () => {
const auth = jest.spyOn(adoTask, 'getEndpointAuthorization');
auth.mockReturnValue(authenticationMock.object);
expect(() => NpmRegistryUtil.getTokenFromServiceConnection('serviceConnectionName')).toThrowError(
'Service connection auth scheme not supported',
);
});
});

describe('Proper Authentications', () => {
it('should get system access token when scheme is OAuth', () => {
authenticationMock.setup((x) => x.scheme).returns(() => 'OAuth');
authenticationMock
.setup((x) => x.parameters)
.returns(() => {
return { AccessToken: 'token' };
});
const auth = jest.spyOn(adoTask, 'getEndpointAuthorization');
auth.mockReturnValue(authenticationMock.object);
const accessToken = NpmRegistryUtil.getSystemAccessToken();
expect(accessToken).toEqual('token');
});

it('should get npmAuthIdent from service connection when service connection scheme is Token', () => {
authenticationMock.setup((x) => x.scheme).returns(() => 'Token');
authenticationMock
.setup((x) => x.parameters)
.returns(() => {
return { apitoken: 'token' };
});
const auth = jest.spyOn(adoTask, 'getEndpointAuthorization');
auth.mockReturnValue(authenticationMock.object);
expect(NpmRegistryUtil.getTokenFromServiceConnection('serviceConnectionName')).toBeTruthy();
});

it('should get npmAuthIdent from service connection when service connection scheme is UsernamePassword', () => {
authenticationMock.setup((x) => x.scheme).returns(() => 'UsernamePassword');
authenticationMock
.setup((x) => x.parameters)
.returns(() => {
return { username: 'username', password: 'password' };
});
const auth = jest.spyOn(adoTask, 'getEndpointAuthorization');
auth.mockReturnValue(authenticationMock.object);
expect(NpmRegistryUtil.getTokenFromServiceConnection('serviceConnectionName')).toBeTruthy();
});
});
});
60 changes: 60 additions & 0 deletions packages/ado-extension/src/npm-registry-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as adoTask from 'azure-pipelines-task-lib/task';

export function getSystemAccessToken(): string {
const authentication = adoTask.getEndpointAuthorization('SYSTEMVSSCONNECTION', false);

if (authentication?.scheme === 'OAuth') {
adoTask.setSecret(authentication.parameters['AccessToken']);
return authentication.parameters['AccessToken'];
} else {
adoTask.warning('Not able to find the credential');
return '';
}
}

export function getTokenFromServiceConnection(serviceConnectionName: string): string {
let serviceConnectionAuth: adoTask.EndpointAuthorization | undefined;
let npmAuthIdent: string;
try {
serviceConnectionAuth = adoTask.getEndpointAuthorization(serviceConnectionName, false);
} catch (exception) {
throw new Error('Could not find the service connection');
}
if (!serviceConnectionAuth) {
throw new Error('Could not find the service connection');
}

const serviceConnectionAuthScheme = serviceConnectionAuth?.scheme;

if (serviceConnectionAuthScheme === 'Token') {
const token = serviceConnectionAuth.parameters['apitoken'];
// to mask the token in pipeline logs
adoTask.setSecret(token);
const usernameToken = `username:${token}`;
adoTask.setSecret(usernameToken);
const base64Token = Buffer.from(usernameToken).toString('base64');
// to mask the token in pipeline logs
adoTask.setSecret(base64Token);
npmAuthIdent = base64Token;
} else if (serviceConnectionAuthScheme === 'UsernamePassword') {
const username = serviceConnectionAuth.parameters['username'];
const password = serviceConnectionAuth.parameters['password'];
// to mask the token in pipeline logs
adoTask.setSecret(password);
const usernamePassword = `${username}:${password}`;
// to mask the token in pipeline logs
adoTask.setSecret(usernamePassword);
const base64Password = Buffer.from(usernamePassword).toString('base64');
// to mask the token in pipeline logs
adoTask.setSecret(base64Password);
npmAuthIdent = base64Password;
} else {
throw new Error('Service connection auth scheme not supported');
}
// to mask the token in pipeline logs
adoTask.setSecret(npmAuthIdent);
return npmAuthIdent;
}
4 changes: 4 additions & 0 deletions packages/ado-extension/src/task-config/ado-task-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,8 @@ export class ADOTaskConfig extends TaskConfig {
const url = 'https://aka.ms/ado-extension-usage';
return url;
}

public getNPMRegistryUrl(): string | undefined {
return this.adoTaskObj.getInput('npmRegistryUrl');
}
}
17 changes: 17 additions & 0 deletions packages/ado-extension/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,23 @@
"required": false,
"helpMarkDown": "Path to Chrome executable. By default, the task downloads a version of Chrome that is tested to work with this task.",
"groupName": "advancedOptions"
},
{
"name": "npmRegistryUrl",
"type": "string",
"label": "NPM registry Url",
"required": false,
"defaultValue": "https://registry.yarnpkg.com",
"helpMarkDown": "NPM registry Url to install the runtime dependencies.",
"groupName": "advancedOptions"
},
{
"name": "npmRegistryCredential",
"label": "Credentials for registries outside this organization/collection",
"helpMarkDown": "Credentials to use for external registries to install the runtime dependencies. For registries in this organization/collection, leave this blank; the build’s credentials are used automatically.",
"type": "connectedService:externalnpmregistry",
"defaultValue": "",
"groupName": "advancedOptions"
}
],
"execution": {
Expand Down

0 comments on commit e31b238

Please sign in to comment.