Skip to content

Commit 3d6209b

Browse files
authored
Merge pull request microsoft#1602 from darsi-an/useEnvVariableToPassPublishToken
[rush] Use environment variable to pass publishing token
2 parents a076332 + a403ec1 commit 3d6209b

6 files changed

Lines changed: 156 additions & 80 deletions

File tree

apps/rush-lib/src/api/RushConfiguration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const DEFAULT_REMOTE: string = 'origin';
4040
*/
4141
const knownRushConfigFilenames: string[] = [
4242
'.npmrc',
43+
'.npmrc-publish',
4344
RushConstants.pinnedVersionsFilename,
4445
RushConstants.commonVersionsFilename,
4546
RushConstants.browserApprovedPackagesFilename,

apps/rush-lib/src/cli/actions/PublishAction.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { VersionControl } from '../../utilities/VersionControl';
2828
import { PolicyValidator } from '../../logic/policy/PolicyValidator';
2929
import { VersionPolicy } from '../../api/VersionPolicy';
3030
import { DEFAULT_PACKAGE_UPDATE_MESSAGE } from './VersionAction';
31+
import { Utilities } from '../../utilities/Utilities';
3132

3233
export class PublishAction extends BaseRushAction {
3334
private _addCommitDetails: CommandLineFlagParameter;
@@ -44,15 +45,16 @@ export class PublishAction extends BaseRushAction {
4445
private _partialPrerelease: CommandLineFlagParameter;
4546
private _suffix: CommandLineStringParameter;
4647
private _force: CommandLineFlagParameter;
47-
private _prereleaseToken: PrereleaseToken;
4848
private _versionPolicy: CommandLineStringParameter;
4949
private _applyGitTagsOnPack: CommandLineFlagParameter;
5050
private _commitId: CommandLineStringParameter;
51-
5251
private _releaseFolder: CommandLineStringParameter;
5352
private _pack: CommandLineFlagParameter;
5453

54+
private _prereleaseToken: PrereleaseToken;
5555
private _hotfixTagOverride: string;
56+
private _targetNpmrcPublishFolder: string;
57+
private _targetNpmrcPublishPath: string;
5658

5759
public constructor(parser: RushCommandLineParser) {
5860
super({
@@ -64,6 +66,12 @@ export class PublishAction extends BaseRushAction {
6466
'changes and publish packages, you must use the --commit flag and/or the --publish flag.',
6567
parser
6668
});
69+
70+
// Example: "common\temp\publish-home"
71+
this._targetNpmrcPublishFolder = path.join(this.rushConfiguration.commonTempFolder, 'publish-home');
72+
73+
// Example: "common\temp\publish-home\.npmrc"
74+
this._targetNpmrcPublishPath = path.join(this._targetNpmrcPublishFolder, '.npmrc');
6775
}
6876

6977
protected onDefineParameters(): void {
@@ -213,6 +221,8 @@ export class PublishAction extends BaseRushAction {
213221

214222
this._validate();
215223

224+
this._addNpmPublishHome();
225+
216226
if (this._includeAll.value) {
217227
this._publishAll(allPackages);
218228
} else {
@@ -401,12 +411,16 @@ export class PublishAction extends BaseRushAction {
401411
const packageManagerToolFilename: string = this.rushConfiguration.packageManager === 'yarn'
402412
? 'npm' : this.rushConfiguration.packageManagerToolFilename;
403413

414+
// If the auth token was specified via the command line, avoid printing it on the console
415+
const secretSubstring: string | undefined = this._npmAuthToken.value;
416+
404417
PublishUtilities.execCommand(
405418
!!this._publish.value,
406419
packageManagerToolFilename,
407420
args,
408421
packagePath,
409-
env);
422+
env,
423+
secretSubstring);
410424
}
411425
}
412426

@@ -486,8 +500,25 @@ export class PublishAction extends BaseRushAction {
486500
}
487501
}
488502

503+
private _addNpmPublishHome(): void {
504+
// Create "common\temp\publish-home" folder, if it doesn't exist
505+
Utilities.createFolderWithRetry(this._targetNpmrcPublishFolder);
506+
507+
// Copy down the committed "common\config\rush\.npmrc-publish" file, if there is one
508+
Utilities.syncNpmrc(this.rushConfiguration.commonRushConfigFolder, this._targetNpmrcPublishFolder, true);
509+
}
510+
489511
private _addSharedNpmConfig(env: { [key: string]: string | undefined }, args: string[]): void {
512+
const userHomeEnvVariable: string = (process.platform === 'win32') ? 'USERPROFILE' : 'HOME';
490513
let registry: string = '//registry.npmjs.org/';
514+
515+
// Check if .npmrc file exists in "common\temp\publish-home"
516+
if (FileSystem.exists(this._targetNpmrcPublishPath)) {
517+
// Redirect userHomeEnvVariable, NPM will use config in "common\temp\publish-home\.npmrc"
518+
env[userHomeEnvVariable] = this._targetNpmrcPublishFolder;
519+
}
520+
521+
// Check if registryUrl and token are specified via command-line
491522
if (this._registryUrl.value) {
492523
const registryUrl: string = this._registryUrl.value;
493524
env['npm_config_registry'] = registryUrl; // eslint-disable-line dot-notation
@@ -498,4 +529,4 @@ export class PublishAction extends BaseRushAction {
498529
args.push(`--${registry}:_authToken=${this._npmAuthToken.value}`);
499530
}
500531
}
501-
}
532+
}

apps/rush-lib/src/logic/PublishUtilities.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
/**
55
* This file contains a set of helper functions that are unit tested and used with the PublishAction,
6-
* which itself it a thin wrapper around these helpers.
6+
* which itself is a thin wrapper around these helpers.
77
*/
88

99
import { EOL } from 'os';
@@ -13,7 +13,8 @@ import * as semver from 'semver';
1313
import {
1414
IPackageJson,
1515
JsonFile,
16-
FileConstants
16+
FileConstants,
17+
Text
1718
} from '@microsoft/node-core-library';
1819

1920
import {
@@ -183,12 +184,17 @@ export class PublishUtilities {
183184
return env;
184185
}
185186

187+
/**
188+
* @param secretSubstring -- if specified, a substring to be replaced by `<<SECRET>>` to avoid printing secrets
189+
* on the console
190+
*/
186191
public static execCommand(
187192
shouldExecute: boolean,
188193
command: string,
189194
args: string[] = [],
190195
workingDirectory: string = process.cwd(),
191-
environment?: IEnvironment
196+
environment?: IEnvironment,
197+
secretSubstring?: string
192198
): void {
193199

194200
let relativeDirectory: string = path.relative(process.cwd(), workingDirectory);
@@ -197,8 +203,15 @@ export class PublishUtilities {
197203
relativeDirectory = `(${relativeDirectory})`;
198204
}
199205

206+
let commandArgs: string = args.join(' ');
207+
208+
if (secretSubstring && secretSubstring.length > 0) {
209+
// Avoid printing the NPM publish token on the console when displaying the commandArgs
210+
commandArgs = Text.replaceAll(commandArgs, secretSubstring, '<<SECRET>>');
211+
}
212+
200213
console.log(
201-
`${EOL}* ${shouldExecute ? 'EXECUTING' : 'DRYRUN'}: ${command} ${args.join(' ')} ${relativeDirectory}`
214+
`${EOL}* ${shouldExecute ? 'EXECUTING' : 'DRYRUN'}: ${command} ${commandArgs} ${relativeDirectory}`
202215
);
203216

204217
if (shouldExecute) {

apps/rush-lib/src/scripts/install-run.ts

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ function _parsePackageSpecifier(rawPackageSpecifier: string): IPackageSpecifier
5252
}
5353

5454
/**
55-
* As a workaround, _syncNpmrc() copies the .npmrc file to the target folder, and also trims
56-
* unusable lines from the .npmrc file. If the source .npmrc file not exist, then _syncNpmrc()
57-
* will delete an .npmrc that is found in the target folder.
55+
* As a workaround, copyAndTrimNpmrcFile() copies the .npmrc file to the target folder, and also trims
56+
* unusable lines from the .npmrc file.
5857
*
5958
* Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in
6059
* the .npmrc file to provide different authentication tokens for different registry.
@@ -63,45 +62,57 @@ function _parsePackageSpecifier(rawPackageSpecifier: string): IPackageSpecifier
6362
* we'd prefer to skip that line and continue looking in other places such as the user's
6463
* home directory.
6564
*
65+
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._copyNpmrcFile()
66+
*/
67+
function _copyAndTrimNpmrcFile(sourceNpmrcPath: string, targetNpmrcPath: string): void {
68+
console.log(`Copying ${sourceNpmrcPath} --> ${targetNpmrcPath}`); // Verbose
69+
let npmrcFileLines: string[] = fs.readFileSync(sourceNpmrcPath).toString().split('\n');
70+
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
71+
const resultLines: string[] = [];
72+
// Trim out lines that reference environment variables that aren't defined
73+
for (const line of npmrcFileLines) {
74+
// This finds environment variable tokens that look like "${VAR_NAME}"
75+
const regex: RegExp = /\$\{([^\}]+)\}/g;
76+
const environmentVariables: string[] | null = line.match(regex);
77+
let lineShouldBeTrimmed: boolean = false;
78+
if (environmentVariables) {
79+
for (const token of environmentVariables) {
80+
// Remove the leading "${" and the trailing "}" from the token
81+
const environmentVariableName: string = token.substring(2, token.length - 1);
82+
if (!process.env[environmentVariableName]) {
83+
lineShouldBeTrimmed = true;
84+
break;
85+
}
86+
}
87+
}
88+
89+
if (lineShouldBeTrimmed) {
90+
// Example output:
91+
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
92+
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
93+
} else {
94+
resultLines.push(line);
95+
}
96+
}
97+
98+
fs.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL));
99+
}
100+
101+
/**
102+
* syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file.
103+
* If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder.
104+
*
66105
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc()
67106
*/
68-
function _syncNpmrc(sourceNpmrcFolder: string, targetNpmrcFolder: string): void {
69-
const sourceNpmrcPath: string = path.join(sourceNpmrcFolder, '.npmrc');
107+
function _syncNpmrc(sourceNpmrcFolder: string, targetNpmrcFolder: string, useNpmrcPublish?: boolean): void {
108+
const sourceNpmrcPath: string = path.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');
70109
const targetNpmrcPath: string = path.join(targetNpmrcFolder, '.npmrc');
71110
try {
72111
if (fs.existsSync(sourceNpmrcPath)) {
73-
let npmrcFileLines: string[] = fs.readFileSync(sourceNpmrcPath).toString().split('\n');
74-
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
75-
const resultLines: string[] = [];
76-
// Trim out lines that reference environment variables that aren't defined
77-
for (const line of npmrcFileLines) {
78-
// This finds environment variable tokens that look like "${VAR_NAME}"
79-
const regex: RegExp = /\$\{([^\}]+)\}/g;
80-
const environmentVariables: string[] | null = line.match(regex);
81-
let lineShouldBeTrimmed: boolean = false;
82-
if (environmentVariables) {
83-
for (const token of environmentVariables) {
84-
// Remove the leading "${" and the trailing "}" from the token
85-
const environmentVariableName: string = token.substring(2, token.length - 1);
86-
if (!process.env[environmentVariableName]) {
87-
lineShouldBeTrimmed = true;
88-
break;
89-
}
90-
}
91-
}
92-
93-
if (lineShouldBeTrimmed) {
94-
// Example output:
95-
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
96-
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
97-
} else {
98-
resultLines.push(line);
99-
}
100-
}
101-
102-
fs.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL));
112+
_copyAndTrimNpmrcFile(sourceNpmrcPath, targetNpmrcPath);
103113
} else if (fs.existsSync(targetNpmrcPath)) {
104114
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
115+
console.log(`Deleting ${targetNpmrcPath}`); // Verbose
105116
fs.unlinkSync(targetNpmrcPath);
106117
}
107118
} catch (e) {

apps/rush-lib/src/utilities/Utilities.ts

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -502,9 +502,8 @@ export class Utilities {
502502
}
503503

504504
/**
505-
* As a workaround, syncNpmrc() copies the .npmrc file to the target folder, and also trims
506-
* unusable lines from the .npmrc file. If the source .npmrc file not exist, then syncNpmrc()
507-
* will delete an .npmrc that is found in the target folder.
505+
* As a workaround, copyAndTrimNpmrcFile() copies the .npmrc file to the target folder, and also trims
506+
* unusable lines from the .npmrc file.
508507
*
509508
* Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in
510509
* the .npmrc file to provide different authentication tokens for different registry.
@@ -513,47 +512,57 @@ export class Utilities {
513512
* we'd prefer to skip that line and continue looking in other places such as the user's
514513
* home directory.
515514
*
515+
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH _copyNpmrcFile() FROM scripts/install-run.ts
516+
*/
517+
public static copyAndTrimNpmrcFile(sourceNpmrcPath: string, targetNpmrcPath: string): void {
518+
console.log(`Copying ${sourceNpmrcPath} --> ${targetNpmrcPath}`); // Verbose
519+
let npmrcFileLines: string[] = FileSystem.readFile(sourceNpmrcPath).split('\n');
520+
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
521+
const resultLines: string[] = [];
522+
// Trim out lines that reference environment variables that aren't defined
523+
for (const line of npmrcFileLines) {
524+
// This finds environment variable tokens that look like "${VAR_NAME}"
525+
const regex: RegExp = /\$\{([^\}]+)\}/g;
526+
const environmentVariables: string[] | null = line.match(regex);
527+
let lineShouldBeTrimmed: boolean = false;
528+
if (environmentVariables) {
529+
for (const token of environmentVariables) {
530+
// Remove the leading "${" and the trailing "}" from the token
531+
const environmentVariableName: string = token.substring(2, token.length - 1);
532+
if (!process.env[environmentVariableName]) {
533+
lineShouldBeTrimmed = true;
534+
break;
535+
}
536+
}
537+
}
538+
539+
if (lineShouldBeTrimmed) {
540+
// Example output:
541+
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
542+
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
543+
} else {
544+
resultLines.push(line);
545+
}
546+
}
547+
548+
FileSystem.writeFile(targetNpmrcPath, resultLines.join(os.EOL));
549+
}
550+
551+
/**
552+
* syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file.
553+
* If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder.
554+
*
516555
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH _syncNpmrc() FROM scripts/install-run.ts
517556
*/
518-
public static syncNpmrc(sourceNpmrcFolder: string, targetNpmrcFolder: string): void {
519-
const sourceNpmrcPath: string = path.join(sourceNpmrcFolder, '.npmrc');
557+
public static syncNpmrc(sourceNpmrcFolder: string, targetNpmrcFolder: string, useNpmrcPublish?: boolean): void {
558+
const sourceNpmrcPath: string = path.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish');
520559
const targetNpmrcPath: string = path.join(targetNpmrcFolder, '.npmrc');
521560
try {
522561
if (FileSystem.exists(sourceNpmrcPath)) {
523-
console.log(`Copying ${sourceNpmrcPath} --> ${targetNpmrcPath}`);
524-
let npmrcFileLines: string[] = FileSystem.readFile(sourceNpmrcPath).split('\n');
525-
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
526-
const resultLines: string[] = [];
527-
// Trim out lines that reference environment variables that aren't defined
528-
for (const line of npmrcFileLines) {
529-
// This finds environment variable tokens that look like "${VAR_NAME}"
530-
const regex: RegExp = /\$\{([^\}]+)\}/g;
531-
const environmentVariables: string[] | null = line.match(regex);
532-
let lineShouldBeTrimmed: boolean = false;
533-
if (environmentVariables) {
534-
for (const token of environmentVariables) {
535-
// Remove the leading "${" and the trailing "}" from the token
536-
const environmentVariableName: string = token.substring(2, token.length - 1);
537-
if (!process.env[environmentVariableName]) {
538-
lineShouldBeTrimmed = true;
539-
break;
540-
}
541-
}
542-
}
543-
544-
if (lineShouldBeTrimmed) {
545-
// Example output:
546-
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
547-
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
548-
} else {
549-
resultLines.push(line);
550-
}
551-
}
552-
553-
FileSystem.writeFile(targetNpmrcPath, resultLines.join(os.EOL));
562+
Utilities.copyAndTrimNpmrcFile(sourceNpmrcPath, targetNpmrcPath);
554563
} else if (FileSystem.exists(targetNpmrcPath)) {
555564
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
556-
console.log(`Deleting ${targetNpmrcPath}`);
565+
console.log(`Deleting ${targetNpmrcPath}`); // Verbose
557566
FileSystem.deleteFile(targetNpmrcPath);
558567
}
559568
} catch (e) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Improve security by allowing the \"rush publish\" authentication token to be specified via an environment variable.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush",
10+
"email": "darsi-an@users.noreply.github.com"
11+
}

0 commit comments

Comments
 (0)