From 8b472d221e53bb6c43f436eb36d30f3afb70fc1a Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Thu, 12 Jul 2018 17:20:26 -0700 Subject: [PATCH] feat(access): Add support for npm access to set per-package 2fa requirements --- lib/access.js | 14 ++++++++++++++ lib/request.js | 35 ++++++++++++++++++++++++----------- test/access.js | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/lib/access.js b/lib/access.js index d8d65a9..caa80b1 100644 --- a/lib/access.js +++ b/lib/access.js @@ -16,6 +16,12 @@ subcommands.public = function (uri, params, cb) { subcommands.restricted = function (uri, params, cb) { return setAccess.call(this, 'restricted', uri, params, cb) } +subcommands['2fa-required'] = function (uri, params, cb) { + return setRequires2fa.call(this, true, uri, params, cb) +} +subcommands['2fa-not-required'] = function (uri, params, cb) { + return setRequires2fa.call(this, false, uri, params, cb) +} function setAccess (access, uri, params, cb) { return this.request(apiUri(uri, 'package', params.package, 'access'), { @@ -25,6 +31,14 @@ function setAccess (access, uri, params, cb) { }, cb) } +function setRequires2fa (requires2fa, uri, params, cb) { + return this.request(apiUri(uri, 'package', params.package, 'access'), { + method: 'POST', + auth: params.auth, + body: JSON.stringify({ publish_requires_tfa: requires2fa }) + }, cb) +} + subcommands.grant = function (uri, params, cb) { var reqUri = apiUri(uri, 'team', params.scope, params.team, 'package') return this.request(reqUri, { diff --git a/lib/request.js b/lib/request.js index 471546c..5987bfa 100644 --- a/lib/request.js +++ b/lib/request.js @@ -289,13 +289,26 @@ function requestDone (method, where, cb) { } if (!parsed.error) { - er = makeError( - 'Registry returned ' + response.statusCode + - ' for ' + method + - ' on ' + where, - name, - response.statusCode - ) + if (response.statusCode === 401 && response.headers['www-authenticate']) { + const auth = response.headers['www-authenticate'].split(/,\s*/).map(s => s.toLowerCase()) + if (auth.indexOf('ipaddress') !== -1) { + er = makeError('Login is not allowed from your IP address', name, response.statusCode, 'EAUTHIP') + } else if (auth.indexOf('otp') !== -1) { + er = makeError('OTP required for this operation', name, response.statusCode, 'EOTP') + } else { + er = makeError('Unable to authenticate, need: ' + response.headers['www-authenticate'], name, response.statusCode, 'EAUTHUNKNOWN') + } + } else { + const msg = parsed.message ? ': ' + parsed.message : '' + er = makeError( + 'Registry returned ' + response.statusCode + + ' for ' + method + + ' on ' + where + + msg, + name, + response.statusCode + ) + } } else if (name && parsed.error === 'not_found') { er = makeError('404 Not Found: ' + name, name, response.statusCode) } else if (name && parsed.error === 'User not found') { @@ -312,12 +325,12 @@ function requestDone (method, where, cb) { }.bind(this) } -function makeError (message, name, code) { +function makeError (message, name, statusCode, code) { var er = new Error(message) if (name) er.pkgid = name - if (code) { - er.statusCode = code - er.code = 'E' + code + if (statusCode) { + er.statusCode = statusCode + er.code = code || 'E' + statusCode } return er } diff --git a/test/access.js b/test/access.js index 78b0001..29f1d3b 100644 --- a/test/access.js +++ b/test/access.js @@ -23,7 +23,7 @@ var UNSCOPED = { } var commands = [ - 'public', 'restricted', 'grant', 'revoke', 'ls-packages', 'ls-collaborators' + 'public', 'restricted', 'grant', 'revoke', 'ls-packages', 'ls-collaborators', '2fa-required', '2fa-not-required' ] test('access public', function (t) { @@ -44,6 +44,40 @@ test('access public', function (t) { }) }) +test('access 2fa-required', function (t) { + server.expect('POST', '/-/package/%40foo%2Fbar/access', function (req, res) { + t.equal(req.method, 'POST', 'requested with POST') + onJsonReq(req, function (json) { + t.deepEqual(json, { publish_requires_tfa: true }, 'request payload ok') + res.statusCode = 200 + res.json('ok') + }) + }) + var params = Object.create(PARAMS) + params.package = '@foo/bar' + client.access('2fa-required', URI, params, function (error, data) { + t.ifError(error, 'no errors') + t.end() + }) +}) + +test('access 2fa-not-required', function (t) { + server.expect('POST', '/-/package/%40foo%2Fbar/access', function (req, res) { + t.equal(req.method, 'POST', 'requested with POST') + onJsonReq(req, function (json) { + t.deepEqual(json, { publish_requires_tfa: false }, 'request payload ok') + res.statusCode = 200 + res.json('ok') + }) + }) + var params = Object.create(PARAMS) + params.package = '@foo/bar' + client.access('2fa-not-required', URI, params, function (error, data) { + t.ifError(error, 'no errors') + t.end() + }) +}) + test('access restricted', function (t) { server.expect('POST', '/-/package/%40foo%2Fbar/access', function (req, res) { t.equal(req.method, 'POST')