token.js 6.9 KB

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