From 9bf2ef5a6f2df3dbbaa25eb7e7243b0732bfaf62 Mon Sep 17 00:00:00 2001 From: isaacs Date: Tue, 15 Dec 2020 15:14:10 -0800 Subject: [PATCH] Support setting email without username/password Fixes: https://github.com/npm/cli/issues/2300 --- lib/index.js | 30 +++++-- tap-snapshots/test-index.js-TAP.test.js | 63 ++++++++++++++ test/index.js | 109 +++++++++++++++++++++++- 3 files changed, 191 insertions(+), 11 deletions(-) diff --git a/lib/index.js b/lib/index.js index a80b976..e7fac96 100644 --- a/lib/index.js +++ b/lib/index.js @@ -178,6 +178,11 @@ class Config { throw new Error('call config.load() before setting values') if (!confTypes.has(where)) throw new Error('invalid config location param: ' + where) + if (key === '_auth') { + const { email } = this.getCredentialsByURI(this.get('registry')) + if (!email) + throw new Error('Cannot set _auth without first setting email') + } this.data.get(where).data[key] = val // this is now dirty, the next call to this.valid will have to check it @@ -512,6 +517,9 @@ class Config { if (where === 'user') { const reg = this.get('registry') const creds = this.getCredentialsByURI(reg) + // we ignore this error because the failed set already removed + // anything that might be a security hazard, and it won't be + // saved back to the .npmrc file, so we're good. try { this.setCredentialsByURI(reg, creds) } catch (_) {} } @@ -576,18 +584,22 @@ class Config { this.delete(`${nerfed}:email`, 'user') this.delete(`${nerfed}:always-auth`, 'user') } else if (username || password || email) { - if (!username) - throw new Error('must include username') - if (!password) - throw new Error('must include password') + if (username || password) { + if (!username) + throw new Error('must include username') + if (!password) + throw new Error('must include password') + } if (!email) throw new Error('must include email') this.delete(`${nerfed}:_authToken`, 'user') - this.set(`${nerfed}:username`, username, 'user') - // note: not encrypted, no idea why we bothered to do this, but oh well - // protects against shoulder-hacks if password is memorable, I guess? - const encoded = Buffer.from(password, 'utf8').toString('base64') - this.set(`${nerfed}:_password`, encoded, 'user') + if (username || password) { + this.set(`${nerfed}:username`, username, 'user') + // note: not encrypted, no idea why we bothered to do this, but oh well + // protects against shoulder-hacks if password is memorable, I guess? + const encoded = Buffer.from(password, 'utf8').toString('base64') + this.set(`${nerfed}:_password`, encoded, 'user') + } this.set(`${nerfed}:email`, email, 'user') if (alwaysAuth !== undefined) this.set(`${nerfed}:always-auth`, alwaysAuth, 'user') diff --git a/tap-snapshots/test-index.js-TAP.test.js b/tap-snapshots/test-index.js-TAP.test.js index 39d8e51..8f9ec4a 100644 --- a/tap-snapshots/test-index.js-TAP.test.js +++ b/tap-snapshots/test-index.js-TAP.test.js @@ -20,6 +20,62 @@ Object { } ` +exports[`test/index.js TAP credentials management def_passNoUser > default registry 1`] = ` +Object { + "alwaysAuth": true, + "email": "i@izs.me", +} +` + +exports[`test/index.js TAP credentials management def_passNoUser > default registry after set 1`] = ` +Object { + "alwaysAuth": true, + "email": "i@izs.me", +} +` + +exports[`test/index.js TAP credentials management def_passNoUser > other registry 1`] = ` +Object { + "alwaysAuth": false, + "email": "i@izs.me", +} +` + +exports[`test/index.js TAP credentials management def_passNoUser > other registry after set 1`] = ` +Object { + "alwaysAuth": false, + "email": "i@izs.me", +} +` + +exports[`test/index.js TAP credentials management def_userNoPass > default registry 1`] = ` +Object { + "alwaysAuth": true, + "email": "i@izs.me", +} +` + +exports[`test/index.js TAP credentials management def_userNoPass > default registry after set 1`] = ` +Object { + "alwaysAuth": true, + "email": "i@izs.me", +} +` + +exports[`test/index.js TAP credentials management def_userNoPass > other registry 1`] = ` +Object { + "alwaysAuth": false, + "email": "i@izs.me", +} +` + +exports[`test/index.js TAP credentials management def_userNoPass > other registry after set 1`] = ` +Object { + "alwaysAuth": false, + "email": "i@izs.me", +} +` + exports[`test/index.js TAP credentials management def_userpass > default registry 1`] = ` Object { "alwaysAuth": true, @@ -47,6 +103,13 @@ Object { } ` +exports[`test/index.js TAP credentials management def_userpass > other registry after set 1`] = ` +Object { + "alwaysAuth": false, + "email": "i@izs.me", +} +` + exports[`test/index.js TAP credentials management nerfed_auth > default registry 1`] = ` Object { "alwaysAuth": false, diff --git a/test/index.js b/test/index.js index a9a8627..53b8e0b 100644 --- a/test/index.js +++ b/test/index.js @@ -562,6 +562,18 @@ t.test('credentials management', async t => { _password = ${Buffer.from('world').toString('base64')} email = i@izs.me //registry.example/:always-auth = true +`, + }, + def_userNoPass: { + '.npmrc': `username = hello +email = i@izs.me +//registry.example/:always-auth = true +`, + }, + def_passNoUser: { + '.npmrc': `_password = ${Buffer.from('world').toString('base64')} +email = i@izs.me +//registry.example/:always-auth = true `, }, def_auth: { @@ -595,6 +607,10 @@ always-auth = true`, username: 'foo', email: 'bar@baz.com', }), { message: 'must include password' }) + t.throws(() => c.setCredentialsByURI('http://x.com', { + password: 'foo', + email: 'bar@baz.com', + }), { message: 'must include username' }) c.setCredentialsByURI('http://x.com', { username: 'foo', password: 'bar', @@ -617,14 +633,15 @@ always-auth = true`, const otherAfterDelete = c.getCredentialsByURI(otherReg) t.strictSame(Object.keys(otherAfterDelete), ['alwaysAuth']) - if (!d.token && !(d.email && d.username && d.password)) + // we can have email on its own, but need both or none of user/pass + if (!d.token && (!d.email || (!!d.username !== !!d.password))) t.throws(() => c.setCredentialsByURI(defReg, d)) else { c.setCredentialsByURI(defReg, d) t.matchSnapshot(c.getCredentialsByURI(defReg), 'default registry after set') } - if (!o.token && !(o.email && o.username && o.password)) + if (!o.token && (!o.email || (!!o.username !== !!o.password))) t.throws(() => c.setCredentialsByURI(otherReg, o)) else { c.setCredentialsByURI(otherReg, o) @@ -754,3 +771,91 @@ t.test('finding the local prefix', t => { }) t.end() }) + +t.test('setting basic auth creds and email', async t => { + const registry = 'https://registry.npmjs.org/' + const path = t.testdir() + const _auth = Buffer.from('admin:admin').toString('base64') + const opts = { + shorthands: {}, + argv: ['node', __filename, `--userconfig=${path}/.npmrc`], + defaults: { + registry, + 'always-auth': false, + }, + types: {}, + npmPath: process.cwd(), + } + const c = new Config(opts) + await c.load() + t.throws(() => c.set('_auth'), 'cannot set _auth without first setting email') + c.set('email', 'name@example.com', 'user') + t.equal(c.get('email', 'user'), 'name@example.com', 'email was set') + await c.save('user') + t.equal(c.get('email', 'user'), undefined, 'email no longer top-level') + t.strictSame(c.getCredentialsByURI(registry), { email: 'name@example.com', alwaysAuth: false }) + const d = new Config(opts) + await d.load() + t.strictSame(d.getCredentialsByURI(registry), { email: 'name@example.com', alwaysAuth: false }) + d.set('_auth', _auth, 'user') + t.equal(d.get('_auth', 'user'), _auth, '_auth was set') + await d.save('user') + t.equal(d.get('_auth', 'user'), undefined, 'un-nerfed _auth deleted') + t.strictSame(d.getCredentialsByURI(registry), { + email: 'name@example.com', + username: 'admin', + password: 'admin', + auth: _auth, + alwaysAuth: false, + }, 'credentials saved and nerfed') +}) + +t.test('setting username/password/email individually', async t => { + const registry = 'https://registry.npmjs.org/' + const path = t.testdir() + const _auth = Buffer.from('admin:admin').toString('base64') + const opts = { + shorthands: {}, + argv: ['node', __filename, `--userconfig=${path}/.npmrc`], + defaults: { + registry, + 'always-auth': false, + }, + types: {}, + npmPath: process.cwd(), + } + const c = new Config(opts) + await c.load() + c.set('email', 'name@example.com', 'user') + t.equal(c.get('email'), 'name@example.com') + c.set('username', 'admin', 'user') + t.equal(c.get('username'), 'admin') + c.set('_password', Buffer.from('admin').toString('base64'), 'user') + t.equal(c.get('_password'), Buffer.from('admin').toString('base64')) + t.equal(c.get('_auth'), undefined) + await c.save('user') + t.equal(c.get('email'), undefined) + t.equal(c.get('username'), undefined) + t.equal(c.get('_password'), undefined) + t.equal(c.get('_auth'), undefined) + t.strictSame(c.getCredentialsByURI(registry), { + alwaysAuth: false, + email: 'name@example.com', + username: 'admin', + password: 'admin', + auth: Buffer.from('admin:admin').toString('base64'), + }) + const d = new Config(opts) + await d.load() + t.equal(d.get('email'), undefined) + t.equal(d.get('username'), undefined) + t.equal(d.get('_password'), undefined) + t.equal(d.get('_auth'), undefined) + t.strictSame(d.getCredentialsByURI(registry), { + alwaysAuth: false, + email: 'name@example.com', + username: 'admin', + password: 'admin', + auth: Buffer.from('admin:admin').toString('base64'), + }) +})