Skip to content

Commit

Permalink
fix: check that npm token has write access on package
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 committed Aug 27, 2024
1 parent ccd6f40 commit 2c69509
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 35 deletions.
52 changes: 26 additions & 26 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { temporaryFile } from "tempy";
import getPkg from "./lib/get-pkg.js";
import verifyNpmConfig from "./lib/verify-config.js";
import verifyNpmAuth from "./lib/verify-auth.js";
import verifyNpmRelease from "./lib/verify-release.js";
import addChannelNpm from "./lib/add-channel.js";
import prepareNpm from "./lib/prepare.js";
import publishNpm from "./lib/publish.js";

let verified;
let releaseVerified;
let prepared;
const npmrc = temporaryFile({ name: ".npmrc" });

Expand Down Expand Up @@ -43,12 +45,13 @@ export async function verifyConditions(pluginConfig, context) {
verified = true;
}

export async function prepare(pluginConfig, context) {
const errors = verified ? [] : verifyNpmConfig(pluginConfig);
export async function verifyRelease(pluginConfig, context) {
const errors = verifyNpmConfig(pluginConfig);

try {
// Reload package.json in case a previous external step updated it
const pkg = await getPkg(pluginConfig, context);

// Verify the npm authentication only if `npmPublish` is not false and `pkg.private` is not `true`
if (!verified && pluginConfig.npmPublish !== false && pkg.private !== true) {
await verifyNpmAuth(npmrc, pkg, context);
}
Expand All @@ -60,19 +63,20 @@ export async function prepare(pluginConfig, context) {
throw new AggregateError(errors);
}

await prepareNpm(npmrc, pluginConfig, context);
prepared = true;
await verifyNpmRelease(npmrc, pkg, context);
releaseVerified = true;
}

export async function publish(pluginConfig, context) {
let pkg;
async function verifyIfNecessary(pluginConfig, context) {
const errors = verified ? [] : verifyNpmConfig(pluginConfig);

let pkg;
try {
// Reload package.json in case a previous external step updated it
pkg = await getPkg(pluginConfig, context);
if (!verified && pluginConfig.npmPublish !== false && pkg.private !== true) {
await verifyNpmAuth(npmrc, pkg, context);
if (pluginConfig.npmPublish !== false && pkg.private !== true) {
if (!verified) await verifyNpmAuth(npmrc, pkg, context);
if (!releaseVerified) await verifyNpmRelease(npmrc, pkg, context);
}
} catch (error) {
errors.push(...error.errors);
Expand All @@ -82,6 +86,18 @@ export async function publish(pluginConfig, context) {
throw new AggregateError(errors);
}

return pkg;
}

export async function prepare(pluginConfig, context) {
await verifyIfNecessary(pluginConfig, context);
await prepareNpm(npmrc, pluginConfig, context);
prepared = true;
}

export async function publish(pluginConfig, context) {
const pkg = await verifyIfNecessary(pluginConfig, context);

if (!prepared) {
await prepareNpm(npmrc, pluginConfig, context);
}
Expand All @@ -90,22 +106,6 @@ export async function publish(pluginConfig, context) {
}

export async function addChannel(pluginConfig, context) {
let pkg;
const errors = verified ? [] : verifyNpmConfig(pluginConfig);

try {
// Reload package.json in case a previous external step updated it
pkg = await getPkg(pluginConfig, context);
if (!verified && pluginConfig.npmPublish !== false && pkg.private !== true) {
await verifyNpmAuth(npmrc, pkg, context);
}
} catch (error) {
errors.push(...error.errors);
}

if (errors.length > 0) {
throw new AggregateError(errors);
}

const pkg = await verifyIfNecessary(pluginConfig, context);
return addChannelNpm(npmrc, pluginConfig, pkg, context);
}
30 changes: 30 additions & 0 deletions lib/npm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export default {
whoami: async ({ userconfig, registry }, execaOpts) => {
const whoamiResult = execa(
"npm",
["whoami", ...(userconfig ? ["--userconfig", userconfig] : []), ...(registry ? ["--registry", registry] : [])],
execaOpts
);
whoamiResult.stdout.pipe(stdout, { end: false });
whoamiResult.stderr.pipe(stderr, { end: false });
return (await whoamiResult).stdout.split("\n").pop();
},
accessListPackages: async ({ principal, pkg, userconfig, registry }, execaOpts) => {
const accessResult = execa(
"npm",
[
"access",
"list",
"packages",
principal,
pkg,
...(userconfig ? ["--userconfig", userconfig] : []),
...(registry ? ["--registry", registry] : []),
],
execaOpts
);
accessResult.stdout.pipe(stdout, { end: false });
accessResult.stderr.pipe(stderr, { end: false });
return Object.fromEntries((await accessResult).stdout.split("\n").map((line) => line.split(/\s*:\s*/)));
},
};
20 changes: 11 additions & 9 deletions lib/verify-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import AggregateError from "aggregate-error";
import getError from "./get-error.js";
import getRegistry from "./get-registry.js";
import setNpmrcAuth from "./set-npmrc-auth.js";
import defaultNpm from "./npm.js";

export default async function (npmrc, pkg, context) {
export default async function (npmrc, pkg, context, npm = defaultNpm) {
const {
cwd,
env: { DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/", ...env },
Expand All @@ -17,15 +18,16 @@ export default async function (npmrc, pkg, context) {
await setNpmrcAuth(npmrc, registry, context);

if (normalizeUrl(registry) === normalizeUrl(DEFAULT_NPM_REGISTRY)) {
let user;
try {
const whoamiResult = execa("npm", ["whoami", "--userconfig", npmrc, "--registry", registry], {
cwd,
env,
preferLocal: true,
});
whoamiResult.stdout.pipe(stdout, { end: false });
whoamiResult.stderr.pipe(stderr, { end: false });
await whoamiResult;
user = await npm.whoami(
{ userconfig: npmrc, registry },
{
cwd,
env,
preferLocal: true,
}
);
} catch {
throw new AggregateError([getError("EINVALIDNPMTOKEN", { registry })]);
}
Expand Down
55 changes: 55 additions & 0 deletions lib/verify-release.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { execa } from "execa";
import normalizeUrl from "normalize-url";
import AggregateError from "aggregate-error";
import getError from "./get-error.js";
import getRegistry from "./get-registry.js";
import setNpmrcAuth from "./set-npmrc-auth.js";
import defaultNpm from "./npm.js";

export default async function (npmrc, pkg, context, npm = defaultNpm) {
const {
cwd,
env: { DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/", ...env },
stdout,
stderr,
} = context;
const registry = getRegistry(pkg, context);

if (context.lastRelease?.version && normalizeUrl(registry) === normalizeUrl(DEFAULT_NPM_REGISTRY)) {
let user;
try {
user = await npm.whoami(
{ userconfig: npmrc, registry },
{
cwd,
env,
preferLocal: true,
}
);
} catch {
throw new AggregateError([getError("EINVALIDNPMTOKEN", { registry })]);
}

let packages;
try {
packages = await npm.accessListPackages(
{
principal: user,
pkg: pkg.name,
userconfig: npmrc,
registry,
},
{
cwd,
env,
preferLocal: true,
}
);
} catch {
throw new AggregateError([getError("EINVALIDNPMTOKEN", { registry })]);
}
if (packages[pkg.name] !== "read-write") {
throw new AggregateError([getError("EINVALIDNPMTOKEN", { registry })]);
}
}
}
1 change: 1 addition & 0 deletions test/integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ test("Publish the package on a dist-tag", async (t) => {
stdout: t.context.stdout,
stderr: t.context.stderr,
logger: t.context.logger,
lastRelease: {},
nextRelease: { channel: "next", version: "1.0.0" },
}
);
Expand Down
44 changes: 44 additions & 0 deletions test/verify-auth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import path from "path";
import test from "ava";
import fs from "fs-extra";
import { temporaryDirectory, temporaryFile } from "tempy";
import { execa } from "execa";
import { stub } from "sinon";
import { WritableStreamBuffer } from "stream-buffers";
import verifyNpmAuth from "../lib/verify-auth.js";

test.beforeEach((t) => {
t.context.log = stub();
t.context.logger = { log: t.context.log };
t.context.stdout = new WritableStreamBuffer();
t.context.stderr = new WritableStreamBuffer();
});

test("Verify valid token and access", async (t) => {
const cwd = temporaryDirectory();
const npmrc = temporaryFile({ name: ".npmrc" });
const packagePath = path.resolve(cwd, "package.json");
const pkg = { name: "foo", version: "0.0.0-dev" };
await fs.outputJson(packagePath, pkg);

const npm = {
whoami: async () => "bob",
accessListPackages: async () => ({}),
};

await t.notThrowsAsync(() =>
verifyNpmAuth(
npmrc,
pkg,
{
cwd,
env: {},
stdout: t.context.stdout,
stderr: t.context.stderr,
nextRelease: { version: "1.0.0" },
logger: t.context.logger,
},
npm
)
);
});
Loading

0 comments on commit 2c69509

Please sign in to comment.