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