1 | 'use strict'
|
2 | const profile = require('npm-profile')
|
3 | const npm = require('./npm.js')
|
4 | const output = require('./utils/output.js')
|
5 | const Table = require('cli-table3')
|
6 | const Bluebird = require('bluebird')
|
7 | const isCidrV4 = require('is-cidr').v4
|
8 | const isCidrV6 = require('is-cidr').v6
|
9 | const readUserInfo = require('./utils/read-user-info.js')
|
10 | const ansistyles = require('ansistyles')
|
11 | const log = require('npmlog')
|
12 | const pulseTillDone = require('./utils/pulse-till-done.js')
|
13 |
|
14 | module.exports = token
|
15 |
|
16 | token._validateCIDRList = validateCIDRList
|
17 |
|
18 | token.usage =
|
19 | 'npm token list\n' +
|
20 | 'npm token revoke <tokenKey>\n' +
|
21 | 'npm token create [--read-only] [--cidr=list]\n'
|
22 |
|
23 | token.subcommands = ['list', 'revoke', 'create']
|
24 |
|
25 | token.completion = function (opts, cb) {
|
26 | var argv = opts.conf.argv.remain
|
27 |
|
28 | switch (argv[2]) {
|
29 | case 'list':
|
30 | case 'revoke':
|
31 | case 'create':
|
32 | return cb(null, [])
|
33 | default:
|
34 | return cb(new Error(argv[2] + ' not recognized'))
|
35 | }
|
36 | }
|
37 |
|
38 | function withCb (prom, cb) {
|
39 | prom.then((value) => cb(null, value), cb)
|
40 | }
|
41 |
|
42 | function token (args, cb) {
|
43 | log.gauge.show('token')
|
44 | if (args.length === 0) return withCb(list([]), cb)
|
45 | switch (args[0]) {
|
46 | case 'list':
|
47 | case 'ls':
|
48 | withCb(list(), cb)
|
49 | break
|
50 | case 'delete':
|
51 | case 'revoke':
|
52 | case 'remove':
|
53 | case 'rm':
|
54 | withCb(rm(args.slice(1)), cb)
|
55 | break
|
56 | case 'create':
|
57 | withCb(create(args.slice(1)), cb)
|
58 | break
|
59 | default:
|
60 | cb(new Error('Unknown profile command: ' + args[0]))
|
61 | }
|
62 | }
|
63 |
|
64 | function generateTokenIds (tokens, minLength) {
|
65 | const byId = {}
|
66 | tokens.forEach((token) => {
|
67 | token.id = token.key
|
68 | for (let ii = minLength; ii < token.key.length; ++ii) {
|
69 | if (!tokens.some((ot) => ot !== token && ot.key.slice(0, ii) === token.key.slice(0, ii))) {
|
70 | token.id = token.key.slice(0, ii)
|
71 | break
|
72 | }
|
73 | }
|
74 | byId[token.id] = token
|
75 | })
|
76 | return byId
|
77 | }
|
78 |
|
79 | function config () {
|
80 | const conf = {
|
81 | json: npm.config.get('json'),
|
82 | parseable: npm.config.get('parseable'),
|
83 | registry: npm.config.get('registry'),
|
84 | otp: npm.config.get('otp')
|
85 | }
|
86 | const creds = npm.config.getCredentialsByURI(conf.registry)
|
87 | if (creds.token) {
|
88 | conf.auth = {token: creds.token}
|
89 | } else if (creds.username) {
|
90 | conf.auth = {basic: {username: creds.username, password: creds.password}}
|
91 | } else if (creds.auth) {
|
92 | const auth = Buffer.from(creds.auth, 'base64').toString().split(':', 2)
|
93 | conf.auth = {basic: {username: auth[0], password: auth[1]}}
|
94 | } else {
|
95 | conf.auth = {}
|
96 | }
|
97 | if (conf.otp) conf.auth.otp = conf.otp
|
98 | return conf
|
99 | }
|
100 |
|
101 | function list (args) {
|
102 | const conf = config()
|
103 | log.info('token', 'getting list')
|
104 | return pulseTillDone.withPromise(profile.listTokens(conf)).then((tokens) => {
|
105 | if (conf.json) {
|
106 | output(JSON.stringify(tokens, null, 2))
|
107 | return
|
108 | } else if (conf.parseable) {
|
109 | output(['key', 'token', 'created', 'readonly', 'CIDR whitelist'].join('\t'))
|
110 | tokens.forEach((token) => {
|
111 | output([
|
112 | token.key,
|
113 | token.token,
|
114 | token.created,
|
115 | token.readonly ? 'true' : 'false',
|
116 | token.cidr_whitelist ? token.cidr_whitelist.join(',') : ''
|
117 | ].join('\t'))
|
118 | })
|
119 | return
|
120 | }
|
121 | generateTokenIds(tokens, 6)
|
122 | const idWidth = tokens.reduce((acc, token) => Math.max(acc, token.id.length), 0)
|
123 | const table = new Table({
|
124 | head: ['id', 'token', 'created', 'readonly', 'CIDR whitelist'],
|
125 | colWidths: [Math.max(idWidth, 2) + 2, 9, 12, 10]
|
126 | })
|
127 | tokens.forEach((token) => {
|
128 | table.push([
|
129 | token.id,
|
130 | token.token + '…',
|
131 | String(token.created).slice(0, 10),
|
132 | token.readonly ? 'yes' : 'no',
|
133 | token.cidr_whitelist ? token.cidr_whitelist.join(', ') : ''
|
134 | ])
|
135 | })
|
136 | output(table.toString())
|
137 | })
|
138 | }
|
139 |
|
140 | function rm (args) {
|
141 | if (args.length === 0) {
|
142 | throw new Error('npm token revoke <tokenKey>')
|
143 | }
|
144 | const conf = config()
|
145 | const toRemove = []
|
146 | const progress = log.newItem('removing tokens', toRemove.length)
|
147 | progress.info('token', 'getting existing list')
|
148 | return pulseTillDone.withPromise(profile.listTokens(conf).then((tokens) => {
|
149 | args.forEach((id) => {
|
150 | const matches = tokens.filter((token) => token.key.indexOf(id) === 0)
|
151 | if (matches.length === 1) {
|
152 | toRemove.push(matches[0].key)
|
153 | } else if (matches.length > 1) {
|
154 | throw new Error(`Token ID "${id}" was ambiguous, a new token may have been created since you last ran \`npm-profile token list\`.`)
|
155 | } else {
|
156 | const tokenMatches = tokens.filter((token) => id.indexOf(token.token) === 0)
|
157 | if (tokenMatches === 0) {
|
158 | throw new Error(`Unknown token id or value "${id}".`)
|
159 | }
|
160 | toRemove.push(id)
|
161 | }
|
162 | })
|
163 | return Bluebird.map(toRemove, (key) => {
|
164 | return profile.removeToken(key, conf).catch((ex) => {
|
165 | if (ex.code !== 'EOTP') throw ex
|
166 | log.info('token', 'failed because revoking this token requires OTP')
|
167 | return readUserInfo.otp().then((otp) => {
|
168 | conf.auth.otp = otp
|
169 | return profile.removeToken(key, conf)
|
170 | })
|
171 | })
|
172 | })
|
173 | })).then(() => {
|
174 | if (conf.json) {
|
175 | output(JSON.stringify(toRemove))
|
176 | } else if (conf.parseable) {
|
177 | output(toRemove.join('\t'))
|
178 | } else {
|
179 | output('Removed ' + toRemove.length + ' token' + (toRemove.length !== 1 ? 's' : ''))
|
180 | }
|
181 | })
|
182 | }
|
183 |
|
184 | function create (args) {
|
185 | const conf = config()
|
186 | const cidr = npm.config.get('cidr')
|
187 | const readonly = npm.config.get('read-only')
|
188 |
|
189 | const validCIDR = validateCIDRList(cidr)
|
190 | return readUserInfo.password().then((password) => {
|
191 | log.info('token', 'creating')
|
192 | return profile.createToken(password, readonly, validCIDR, conf).catch((ex) => {
|
193 | if (ex.code !== 'EOTP') throw ex
|
194 | log.info('token', 'failed because it requires OTP')
|
195 | return readUserInfo.otp().then((otp) => {
|
196 | conf.auth.otp = otp
|
197 | log.info('token', 'creating with OTP')
|
198 | return pulseTillDone.withPromise(profile.createToken(password, readonly, validCIDR, conf))
|
199 | })
|
200 | })
|
201 | }).then((result) => {
|
202 | delete result.key
|
203 | delete result.updated
|
204 | if (conf.json) {
|
205 | output(JSON.stringify(result))
|
206 | } else if (conf.parseable) {
|
207 | Object.keys(result).forEach((k) => output(k + '\t' + result[k]))
|
208 | } else {
|
209 | const table = new Table()
|
210 | Object.keys(result).forEach((k) => table.push({[ansistyles.bright(k)]: String(result[k])}))
|
211 | output(table.toString())
|
212 | }
|
213 | })
|
214 | }
|
215 |
|
216 | function validateCIDR (cidr) {
|
217 | if (isCidrV6(cidr)) {
|
218 | throw new Error('CIDR whitelist can only contain IPv4 addresses, ' + cidr + ' is IPv6')
|
219 | }
|
220 | if (!isCidrV4(cidr)) {
|
221 | throw new Error('CIDR whitelist contains invalid CIDR entry: ' + cidr)
|
222 | }
|
223 | }
|
224 |
|
225 | function validateCIDRList (cidrs) {
|
226 | const maybeList = cidrs ? (Array.isArray(cidrs) ? cidrs : [cidrs]) : []
|
227 | const list = maybeList.length === 1 ? maybeList[0].split(/,\s*/) : maybeList
|
228 | list.forEach(validateCIDR)
|
229 | return list
|
230 | }
|