Skip to content

Commit 8b472d2

Browse files
committed
feat(access): Add support for npm access to set per-package 2fa requirements
1 parent 935d085 commit 8b472d2

File tree

3 files changed

+73
-12
lines changed

3 files changed

+73
-12
lines changed

lib/access.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ subcommands.public = function (uri, params, cb) {
1616
subcommands.restricted = function (uri, params, cb) {
1717
return setAccess.call(this, 'restricted', uri, params, cb)
1818
}
19+
subcommands['2fa-required'] = function (uri, params, cb) {
20+
return setRequires2fa.call(this, true, uri, params, cb)
21+
}
22+
subcommands['2fa-not-required'] = function (uri, params, cb) {
23+
return setRequires2fa.call(this, false, uri, params, cb)
24+
}
1925

2026
function setAccess (access, uri, params, cb) {
2127
return this.request(apiUri(uri, 'package', params.package, 'access'), {
@@ -25,6 +31,14 @@ function setAccess (access, uri, params, cb) {
2531
}, cb)
2632
}
2733

34+
function setRequires2fa (requires2fa, uri, params, cb) {
35+
return this.request(apiUri(uri, 'package', params.package, 'access'), {
36+
method: 'POST',
37+
auth: params.auth,
38+
body: JSON.stringify({ publish_requires_tfa: requires2fa })
39+
}, cb)
40+
}
41+
2842
subcommands.grant = function (uri, params, cb) {
2943
var reqUri = apiUri(uri, 'team', params.scope, params.team, 'package')
3044
return this.request(reqUri, {

lib/request.js

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -289,13 +289,26 @@ function requestDone (method, where, cb) {
289289
}
290290

291291
if (!parsed.error) {
292-
er = makeError(
293-
'Registry returned ' + response.statusCode +
294-
' for ' + method +
295-
' on ' + where,
296-
name,
297-
response.statusCode
298-
)
292+
if (response.statusCode === 401 && response.headers['www-authenticate']) {
293+
const auth = response.headers['www-authenticate'].split(/,\s*/).map(s => s.toLowerCase())
294+
if (auth.indexOf('ipaddress') !== -1) {
295+
er = makeError('Login is not allowed from your IP address', name, response.statusCode, 'EAUTHIP')
296+
} else if (auth.indexOf('otp') !== -1) {
297+
er = makeError('OTP required for this operation', name, response.statusCode, 'EOTP')
298+
} else {
299+
er = makeError('Unable to authenticate, need: ' + response.headers['www-authenticate'], name, response.statusCode, 'EAUTHUNKNOWN')
300+
}
301+
} else {
302+
const msg = parsed.message ? ': ' + parsed.message : ''
303+
er = makeError(
304+
'Registry returned ' + response.statusCode +
305+
' for ' + method +
306+
' on ' + where +
307+
msg,
308+
name,
309+
response.statusCode
310+
)
311+
}
299312
} else if (name && parsed.error === 'not_found') {
300313
er = makeError('404 Not Found: ' + name, name, response.statusCode)
301314
} else if (name && parsed.error === 'User not found') {
@@ -312,12 +325,12 @@ function requestDone (method, where, cb) {
312325
}.bind(this)
313326
}
314327

315-
function makeError (message, name, code) {
328+
function makeError (message, name, statusCode, code) {
316329
var er = new Error(message)
317330
if (name) er.pkgid = name
318-
if (code) {
319-
er.statusCode = code
320-
er.code = 'E' + code
331+
if (statusCode) {
332+
er.statusCode = statusCode
333+
er.code = code || 'E' + statusCode
321334
}
322335
return er
323336
}

test/access.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ var UNSCOPED = {
2323
}
2424

2525
var commands = [
26-
'public', 'restricted', 'grant', 'revoke', 'ls-packages', 'ls-collaborators'
26+
'public', 'restricted', 'grant', 'revoke', 'ls-packages', 'ls-collaborators', '2fa-required', '2fa-not-required'
2727
]
2828

2929
test('access public', function (t) {
@@ -44,6 +44,40 @@ test('access public', function (t) {
4444
})
4545
})
4646

47+
test('access 2fa-required', function (t) {
48+
server.expect('POST', '/-/package/%40foo%2Fbar/access', function (req, res) {
49+
t.equal(req.method, 'POST', 'requested with POST')
50+
onJsonReq(req, function (json) {
51+
t.deepEqual(json, { publish_requires_tfa: true }, 'request payload ok')
52+
res.statusCode = 200
53+
res.json('ok')
54+
})
55+
})
56+
var params = Object.create(PARAMS)
57+
params.package = '@foo/bar'
58+
client.access('2fa-required', URI, params, function (error, data) {
59+
t.ifError(error, 'no errors')
60+
t.end()
61+
})
62+
})
63+
64+
test('access 2fa-not-required', function (t) {
65+
server.expect('POST', '/-/package/%40foo%2Fbar/access', function (req, res) {
66+
t.equal(req.method, 'POST', 'requested with POST')
67+
onJsonReq(req, function (json) {
68+
t.deepEqual(json, { publish_requires_tfa: false }, 'request payload ok')
69+
res.statusCode = 200
70+
res.json('ok')
71+
})
72+
})
73+
var params = Object.create(PARAMS)
74+
params.package = '@foo/bar'
75+
client.access('2fa-not-required', URI, params, function (error, data) {
76+
t.ifError(error, 'no errors')
77+
t.end()
78+
})
79+
})
80+
4781
test('access restricted', function (t) {
4882
server.expect('POST', '/-/package/%40foo%2Fbar/access', function (req, res) {
4983
t.equal(req.method, 'POST')

0 commit comments

Comments
 (0)