From a5b37045032e79c0b76f885c620d09c9331fc86c Mon Sep 17 00:00:00 2001 From: Jeremi Piotrowski Date: Mon, 29 Jul 2024 13:09:20 +0000 Subject: [PATCH 1/4] ado-npm-auth: Add help text Add support for `-h` or `--help` cli flag and print the supported options. Signed-off-by: Jeremi Piotrowski --- packages/ado-npm-auth/src/args.ts | 13 +++++++++++++ packages/ado-npm-auth/src/cli.ts | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/ado-npm-auth/src/args.ts b/packages/ado-npm-auth/src/args.ts index 2758cf9b..84eb46ed 100644 --- a/packages/ado-npm-auth/src/args.ts +++ b/packages/ado-npm-auth/src/args.ts @@ -2,14 +2,27 @@ export interface Args { doValidCheck: boolean; skipAuth: boolean; configFile?: string; + help: boolean; +} + +export function printHelp() { + console.log(` +Usage: + + -h --help Show this + --skip-auth Don't authenticate + --skip-check Don't check whether auth is still valid +`) } export function parseArgs(args: string[]): Args { const doValidCheck = !args.includes("--skip-check"); const skipAuth = args.includes("--skip-auth"); + const help = args.includes('--help') || args.includes('-h'); return { doValidCheck, skipAuth, + help, }; } diff --git a/packages/ado-npm-auth/src/cli.ts b/packages/ado-npm-auth/src/cli.ts index c89af4e9..edd6a077 100644 --- a/packages/ado-npm-auth/src/cli.ts +++ b/packages/ado-npm-auth/src/cli.ts @@ -2,7 +2,7 @@ import { isSupportedPlatformAndArchitecture } from "./azureauth/is-supported-pla import { isCodespaces } from "./utils/is-codespaces.js"; import { logTelemetry } from "./telemetry/index.js"; import { arch, platform } from "os"; -import { Args, parseArgs } from "./args.js"; +import { Args, parseArgs, printHelp } from "./args.js"; import { NpmrcFileProvider } from "./npmrc/npmrcFileProvider.js"; import { defaultEmail, defaultUser, ValidatedFeed } from "./fileProvider.js"; import { generateNpmrcPat } from "./npmrc/generate-npmrc-pat.js"; @@ -115,6 +115,10 @@ if (!isSupportedPlatformAndArchitecture()) { } const args = parseArgs(process.argv); +if (args.help) { + printHelp(); + process.exit(0) +} const result = await run(args); From 7d189229ea5b29c8922e0b4dc3e76e8016c8aca5 Mon Sep 17 00:00:00 2001 From: Jeremi Piotrowski Date: Mon, 29 Jul 2024 13:28:25 +0000 Subject: [PATCH 2/4] Support native linux usage azureauth publishes linux deb files since v0.8.2. Add linux/x64 and linux/arm64 to the supported configuration list so that ado-npm-auth can be used there. Scripts/install.js relies on dpkg-deb to unpack the deb file, which is fine since the binaries are built for/on Ubuntu where dpkg-deb is guaranteed to be available. The linux binaries might run on other distros. --- .../is-supported-platform-and-architecture.ts | 1 + packages/node-azureauth/scripts/install.js | 25 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/ado-npm-auth/src/azureauth/is-supported-platform-and-architecture.ts b/packages/ado-npm-auth/src/azureauth/is-supported-platform-and-architecture.ts index 14aa74b6..489e8277 100644 --- a/packages/ado-npm-auth/src/azureauth/is-supported-platform-and-architecture.ts +++ b/packages/ado-npm-auth/src/azureauth/is-supported-platform-and-architecture.ts @@ -8,6 +8,7 @@ import { isWsl } from "../utils/is-wsl.js"; export const isSupportedPlatformAndArchitecture = (): boolean => { const supportedPlatformsAndArchitectures: Record = { win32: ["x64"], + linux: ["x64", "arm64"], darwin: ["x64", "arm64"], }; diff --git a/packages/node-azureauth/scripts/install.js b/packages/node-azureauth/scripts/install.js index 5d91eed3..344b7003 100644 --- a/packages/node-azureauth/scripts/install.js +++ b/packages/node-azureauth/scripts/install.js @@ -3,6 +3,9 @@ import fs from "fs"; import { DownloaderHelper } from "node-downloader-helper"; import decompress from "decompress"; import { fileURLToPath } from "url"; +import { promisify } from "node:util"; +import { execFile as _execFile } from "node:child_process"; +const execFile = promisify(_execFile); const __dirname = fileURLToPath(new URL(".", import.meta.url)); const AZURE_AUTH_VERSION = "0.8.4"; @@ -38,7 +41,7 @@ const AZUREAUTH_INFO = { const AZUREAUTH_NAME_MAP = { def: "azureauth", win32: "azureauth.exe", - linux: "azureauth.exe", + linux: "azureauth", }; export const AZUREAUTH_NAME = @@ -75,10 +78,10 @@ export const install = async () => { arm64: `azureauth-${AZUREAUTH_INFO.version}-osx-arm64.tar.gz`, }, // TODO: support linux when the binaries are available - // linux: { - // def: "azureauth.exe", - // x64: "azureauth-${AZUREAUTH_INFO.version}-win10-x64.zip", - // }, + linux: { + x64: `azureauth-${AZUREAUTH_INFO.version}-linux-x64.deb`, + arm64: `azureauth-${AZUREAUTH_INFO.version}-linux-arm64.deb`, + }, }; if (platform in DOWNLOAD_MAP) { // download the executable @@ -107,7 +110,17 @@ export const install = async () => { const binaryPath = path.join(distPath, AZUREAUTH_NAME); - await decompress(archivePath, distPath); + if (platform == "linux") { + try { + await execFile("dpkg-deb", ["--extract", archivePath, distPath]) + } catch (err) { + fs.unlinkSync(archivePath); + throw err; + } + fs.symlinkSync("usr/lib/azureauth/azureauth", binaryPath) + } else { + await decompress(archivePath, distPath); + } if (fileExist(binaryPath)) { fs.chmodSync(binaryPath, fs.constants.S_IXUSR || 0o100); From 6af0f83b5dc74a1e92841c381f6a43fda4400798 Mon Sep 17 00:00:00 2001 From: Jeremi Piotrowski Date: Mon, 29 Jul 2024 14:56:00 +0000 Subject: [PATCH 3/4] ado-npm-auth: Support device code flow When running headless on linux we can't rely on web flow for authentication. Add a cli argument that forces azureauth into devicecode flow to support that scenario. Move from running azureauth with `exec` to `spawn` to make this work. We need to use spawn so that stderr can be inherited from the calling process. The device code is displayed to the user on stderr. Signed-off-by: Jeremi Piotrowski --- packages/ado-npm-auth/src/args.ts | 4 +++ packages/ado-npm-auth/src/azureauth/ado.ts | 14 +++++------ packages/ado-npm-auth/src/cli.ts | 2 +- .../src/npmrc/generate-npmrc-pat.ts | 3 +++ packages/ado-npm-auth/src/utils/exec.ts | 25 ++++++++++++++++++- 5 files changed, 39 insertions(+), 9 deletions(-) diff --git a/packages/ado-npm-auth/src/args.ts b/packages/ado-npm-auth/src/args.ts index 84eb46ed..18e6efe6 100644 --- a/packages/ado-npm-auth/src/args.ts +++ b/packages/ado-npm-auth/src/args.ts @@ -3,6 +3,7 @@ export interface Args { skipAuth: boolean; configFile?: string; help: boolean; + deviceCode: boolean; } export function printHelp() { @@ -12,6 +13,7 @@ Usage: -h --help Show this --skip-auth Don't authenticate --skip-check Don't check whether auth is still valid + --device-code Use device code flow for authentication `) } @@ -19,10 +21,12 @@ export function parseArgs(args: string[]): Args { const doValidCheck = !args.includes("--skip-check"); const skipAuth = args.includes("--skip-auth"); const help = args.includes('--help') || args.includes('-h'); + const deviceCode = args.includes('--device-code'); return { doValidCheck, skipAuth, help, + deviceCode, }; } diff --git a/packages/ado-npm-auth/src/azureauth/ado.ts b/packages/ado-npm-auth/src/azureauth/ado.ts index 53467a70..d11b1f18 100644 --- a/packages/ado-npm-auth/src/azureauth/ado.ts +++ b/packages/ado-npm-auth/src/azureauth/ado.ts @@ -1,5 +1,5 @@ import { arch, platform } from "os"; -import { exec } from "../utils/exec.js"; +import { spawn } from "../utils/exec.js"; import { isSupportedPlatformAndArchitecture } from "./is-supported-platform-and-architecture.js"; import { azureAuthCommand } from "./azureauth-command.js"; import { isWsl } from "../utils/is-wsl.js"; @@ -47,7 +47,7 @@ export const adoPat = async ( ...authCommand, `ado`, `pat`, - `--prompt-hint ${isWsl() ? options.promptHint : `"${options.promptHint}"`}`, // We only use spawn for WSL. spawn does not does not require prompt hint to be wrapped in quotes. exec does. + `--prompt-hint ${isWsl() ? options.promptHint : `"${options.promptHint}"`}`, // Prompt hint may contain spaces so wrap in quotes. `--organization ${options.organization}`, `--display-name ${options.displayName}`, ...options.scope.map((scope) => `--scope ${scope}`), @@ -85,11 +85,11 @@ export const adoPat = async ( } } else { try { - result = await exec(command.join(" "), { env }); - - if (result.stderr) { - throw new Error(result.stderr); - } + result = await spawn(command[0], command.slice(1), { + shell: false, + env, + stdio: ["ignore", "pipe", "inherit"], + }); } catch (error: any) { throw new Error( `Failed to get Ado Pat from npx AzureAuth: ${error.message}`, diff --git a/packages/ado-npm-auth/src/cli.ts b/packages/ado-npm-auth/src/cli.ts index edd6a077..71448ddd 100644 --- a/packages/ado-npm-auth/src/cli.ts +++ b/packages/ado-npm-auth/src/cli.ts @@ -54,7 +54,7 @@ export const run = async (args: Args): Promise => { // get a token for each feed const organizationPatMap: Record = {}; for (const adoOrg of adoOrgs) { - organizationPatMap[adoOrg] = await generateNpmrcPat(adoOrg, false); + organizationPatMap[adoOrg] = await generateNpmrcPat(adoOrg, false, args.deviceCode); } // Update the pat in the invalid feeds. diff --git a/packages/ado-npm-auth/src/npmrc/generate-npmrc-pat.ts b/packages/ado-npm-auth/src/npmrc/generate-npmrc-pat.ts index b3cc9808..dbec4257 100644 --- a/packages/ado-npm-auth/src/npmrc/generate-npmrc-pat.ts +++ b/packages/ado-npm-auth/src/npmrc/generate-npmrc-pat.ts @@ -9,8 +9,10 @@ import { toBase64 } from "../utils/encoding.js"; export const generateNpmrcPat = async ( organization: string, encode = false, + devicecode = false, ): Promise => { const name = `${hostname()}-${organization}`; + const mode = devicecode ? "devicecode" : ""; const pat = await adoPat({ promptHint: `${name} .npmrc PAT`, organization, @@ -18,6 +20,7 @@ export const generateNpmrcPat = async ( scope: ["vso.packaging"], timeout: "30", output: "json", + mode, }); const rawToken = (pat as AdoPatResponse).token; diff --git a/packages/ado-npm-auth/src/utils/exec.ts b/packages/ado-npm-auth/src/utils/exec.ts index c3b7e94b..2de5c61c 100644 --- a/packages/ado-npm-auth/src/utils/exec.ts +++ b/packages/ado-npm-auth/src/utils/exec.ts @@ -1,4 +1,27 @@ -import { exec as _exec } from "node:child_process"; +import { exec as _exec, spawn as _spawn } from "node:child_process"; import { promisify } from "node:util"; export const exec = promisify(_exec); +export const spawn = ( + cmd: string, + args: ReadonlyArray, + opts: Object, +): Promise<{ stdout: string }> => { + let result = _spawn(cmd, args, opts); + return new Promise((resolve, reject) => { + let stdout = ""; + result.stdout.on("data", (data) => { + stdout += data; + }); + result.on("close", (code) => { + if (code == 0) { + resolve({ stdout }); + } else { + reject(new Error(`process exited with error code: ${code}`)); + } + }); + result.on("error", (err) => { + reject(err); + }); + }); +}; From 806abb584707e08b80df5e16e2267f80c77952e1 Mon Sep 17 00:00:00 2001 From: Jeremi Piotrowski Date: Wed, 31 Jul 2024 09:38:52 +0000 Subject: [PATCH 4/4] ado-npm-auth: Fix tests after moving to spawn Signed-off-by: Jeremi Piotrowski --- .../ado-npm-auth/src/azureauth/ado.test.ts | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/ado-npm-auth/src/azureauth/ado.test.ts b/packages/ado-npm-auth/src/azureauth/ado.test.ts index 4fe9662c..67175091 100644 --- a/packages/ado-npm-auth/src/azureauth/ado.test.ts +++ b/packages/ado-npm-auth/src/azureauth/ado.test.ts @@ -1,6 +1,6 @@ import { expect, test, vi, beforeEach } from "vitest"; import { AdoPatResponse, adoPat } from "./ado.js"; -import { exec } from "../utils/exec.js"; +import { spawn } from "../utils/exec.js"; import { spawnSync } from "child_process"; import * as utils from "../utils/is-wsl.js"; @@ -21,7 +21,7 @@ vi.mock("./is-azureauth-installed.js", async () => { vi.mock("../utils/exec.js", async () => { return { - exec: vi.fn(), + spawn: vi.fn(), }; }); @@ -43,7 +43,7 @@ beforeEach(() => { test("it should spawn azureauth", async () => { vi.mocked(utils.isWsl).mockReturnValue(false); - vi.mocked(exec).mockReturnValue( + vi.mocked(spawn).mockReturnValue( Promise.resolve({ stdout: '{ "token": "foobarabc123" }', stderr: "", @@ -60,8 +60,24 @@ test("it should spawn azureauth", async () => { timeout: "200", })) as AdoPatResponse; - expect(exec).toHaveBeenCalledWith( - 'npm exec --silent --yes azureauth -- ado pat --prompt-hint "hint" --organization org --display-name test display --scope foobar --output json --domain baz.com --timeout 200', + expect(spawn).toHaveBeenCalledWith( + "npm", + [ + "exec", + "--silent", + "--yes", + "azureauth", + "--", + "ado", + "pat", + '--prompt-hint "hint"', + "--organization org", + "--display-name test display", + "--scope foobar", + "--output json", + "--domain baz.com", + "--timeout 200", + ], expect.anything(), ); expect(results.token).toBe("foobarabc123"); @@ -108,7 +124,7 @@ test("it should spawnSync azureauth on wsl", async () => { test("it should handle json errors", async () => { vi.mocked(utils.isWsl).mockReturnValue(false); - vi.mocked(exec).mockReturnValue( + vi.mocked(spawn).mockReturnValue( Promise.resolve({ stdout: "an error", stderr: "", @@ -130,12 +146,7 @@ test("it should handle json errors", async () => { test("it should handle errors from azureauth-cli", async () => { vi.mocked(utils.isWsl).mockReturnValue(false); - vi.mocked(exec).mockReturnValue( - Promise.resolve({ - stdout: "", - stderr: "an error", - }) as any, - ); + vi.mocked(spawn).mockReturnValue(Promise.reject(new Error("an error"))); await expect( adoPat({