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