token.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. const { log, output, META } = require('proc-log')
  2. const fetch = require('npm-registry-fetch')
  3. const { otplease } = require('../utils/auth.js')
  4. const readUserInfo = require('../utils/read-user-info.js')
  5. const BaseCommand = require('../base-cmd.js')
  6. async function paginate (href, opts, items = []) {
  7. while (href) {
  8. const result = await fetch.json(href, opts)
  9. items = items.concat(result.objects)
  10. href = result.urls.next
  11. }
  12. return items
  13. }
  14. class Token extends BaseCommand {
  15. static description = 'Manage your authentication tokens'
  16. static name = 'token'
  17. static usage = ['list', 'revoke <id|token>', 'create']
  18. static params = ['name',
  19. 'token-description',
  20. 'expires',
  21. 'packages',
  22. 'packages-all',
  23. 'scopes',
  24. 'orgs',
  25. 'packages-and-scopes-permission',
  26. 'orgs-permission',
  27. 'cidr',
  28. 'bypass-2fa',
  29. 'password',
  30. 'registry',
  31. 'otp',
  32. 'read-only',
  33. ]
  34. static async completion (opts) {
  35. const argv = opts.conf.argv.remain
  36. const subcommands = ['list', 'revoke', 'create']
  37. if (argv.length === 2) {
  38. return subcommands
  39. }
  40. if (subcommands.includes(argv[2])) {
  41. return []
  42. }
  43. throw new Error(argv[2] + ' not recognized')
  44. }
  45. async exec (args) {
  46. if (args.length === 0) {
  47. return this.list()
  48. }
  49. switch (args[0]) {
  50. case 'list':
  51. case 'ls':
  52. return this.list()
  53. case 'rm':
  54. case 'delete':
  55. case 'revoke':
  56. case 'remove':
  57. return this.rm(args.slice(1))
  58. case 'create':
  59. return this.create(args.slice(1))
  60. default:
  61. throw this.usageError(`${args[0]} is not a recognized subcommand.`)
  62. }
  63. }
  64. async list () {
  65. const json = this.npm.config.get('json')
  66. const parseable = this.npm.config.get('parseable')
  67. log.info('token', 'getting list')
  68. const tokens = await paginate('/-/npm/v1/tokens', this.npm.flatOptions)
  69. if (json) {
  70. output.buffer(tokens)
  71. return
  72. }
  73. if (parseable) {
  74. output.standard(['key', 'token', 'created', 'readonly', 'CIDR whitelist'].join('\t'))
  75. tokens.forEach(token => {
  76. output.standard(
  77. [
  78. token.key,
  79. token.token,
  80. token.created,
  81. token.readonly ? 'true' : 'false',
  82. token.cidr_whitelist ? token.cidr_whitelist.join(',') : '',
  83. ].join('\t')
  84. )
  85. })
  86. return
  87. }
  88. this.generateTokenIds(tokens, 6)
  89. const chalk = this.npm.chalk
  90. for (const token of tokens) {
  91. const created = String(token.created).slice(0, 10)
  92. output.standard(`${chalk.blue('Token')} ${token.token}… with id ${chalk.cyan(token.id)} created ${created}`)
  93. if (token.cidr_whitelist) {
  94. output.standard(`with IP whitelist: ${chalk.green(token.cidr_whitelist.join(','))}`)
  95. }
  96. output.standard()
  97. }
  98. }
  99. async rm (args) {
  100. if (args.length === 0) {
  101. throw this.usageError('`<tokenKey>` argument is required.')
  102. }
  103. const json = this.npm.config.get('json')
  104. const parseable = this.npm.config.get('parseable')
  105. const toRemove = []
  106. log.info('token', `removing ${toRemove.length} tokens`)
  107. const tokens = await paginate('/-/npm/v1/tokens', this.npm.flatOptions)
  108. for (const id of args) {
  109. const matches = tokens.filter(token => token.key.indexOf(id) === 0)
  110. if (matches.length === 1) {
  111. toRemove.push(matches[0].key)
  112. } else if (matches.length > 1) {
  113. throw new Error(
  114. `Token ID "${id}" was ambiguous, a new token may have been created since you last ran \`npm token list\`.`
  115. )
  116. } else {
  117. const tokenMatches = tokens.some(t => id.indexOf(t.token) === 0)
  118. if (!tokenMatches) {
  119. throw new Error(`Unknown token id or value "${id}".`)
  120. }
  121. toRemove.push(id)
  122. }
  123. }
  124. for (const tokenKey of toRemove) {
  125. await otplease(this.npm, this.npm.flatOptions, opts =>
  126. fetch(`/-/npm/v1/tokens/token/${tokenKey}`, {
  127. ...opts,
  128. method: 'DELETE',
  129. ignoreBody: true,
  130. })
  131. )
  132. }
  133. if (json) {
  134. output.buffer(toRemove)
  135. } else if (parseable) {
  136. output.standard(toRemove.join('\t'))
  137. } else {
  138. output.standard('Removed ' + toRemove.length + ' token' + (toRemove.length !== 1 ? 's' : ''))
  139. }
  140. }
  141. async create () {
  142. const json = this.npm.config.get('json')
  143. const parseable = this.npm.config.get('parseable')
  144. const cidr = this.npm.config.get('cidr')
  145. const name = this.npm.config.get('name')
  146. const tokenDescription = this.npm.config.get('token-description')
  147. const expires = this.npm.config.get('expires')
  148. const packages = this.npm.config.get('packages')
  149. const packagesAll = this.npm.config.get('packages-all')
  150. const scopes = this.npm.config.get('scopes')
  151. const orgs = this.npm.config.get('orgs')
  152. const packagesAndScopesPermission = this.npm.config.get('packages-and-scopes-permission')
  153. const orgsPermission = this.npm.config.get('orgs-permission')
  154. const bypassTwoFactor = this.npm.config.get('bypass-2fa')
  155. let password = this.npm.config.get('password')
  156. const validCIDR = await this.validateCIDRList(cidr)
  157. /* istanbul ignore if - skip testing read input */
  158. if (!password) {
  159. password = await readUserInfo.password()
  160. }
  161. const tokenData = {
  162. name: name,
  163. password: password,
  164. }
  165. if (tokenDescription) {
  166. tokenData.description = tokenDescription
  167. }
  168. if (packages?.length > 0) {
  169. tokenData.packages = packages
  170. }
  171. if (packagesAll) {
  172. tokenData.packages_all = true
  173. }
  174. if (scopes?.length > 0) {
  175. tokenData.scopes = scopes
  176. }
  177. if (orgs?.length > 0) {
  178. tokenData.orgs = orgs
  179. }
  180. if (packagesAndScopesPermission) {
  181. tokenData.packages_and_scopes_permission = packagesAndScopesPermission
  182. }
  183. if (orgsPermission) {
  184. tokenData.orgs_permission = orgsPermission
  185. }
  186. // Add expiration in days
  187. if (expires) {
  188. tokenData.expires = parseInt(expires, 10)
  189. }
  190. // Add optional fields
  191. if (validCIDR?.length > 0) {
  192. tokenData.cidr_whitelist = validCIDR
  193. }
  194. if (bypassTwoFactor) {
  195. tokenData.bypass_2fa = true
  196. }
  197. log.info('token', 'creating')
  198. const result = await otplease(this.npm, this.npm.flatOptions, opts =>
  199. fetch.json('/-/npm/v1/tokens', {
  200. ...opts,
  201. method: 'POST',
  202. body: tokenData,
  203. })
  204. )
  205. delete result.key
  206. delete result.updated
  207. if (json) {
  208. output.buffer(result)
  209. } else if (parseable) {
  210. Object.keys(result).forEach(k => output.standard(k + '\t' + result[k]))
  211. } else {
  212. const chalk = this.npm.chalk
  213. output.standard(`Created token ${result.token}`, { [META]: true, redact: false })
  214. if (result.cidr_whitelist?.length) {
  215. output.standard(`with IP whitelist: ${chalk.green(result.cidr_whitelist.join(','))}`)
  216. }
  217. if (result.expires) {
  218. output.standard(`expires: ${result.expires}`)
  219. }
  220. }
  221. }
  222. invalidCIDRError (msg) {
  223. return Object.assign(new Error(msg), { code: 'EINVALIDCIDR' })
  224. }
  225. generateTokenIds (tokens, minLength) {
  226. for (const token of tokens) {
  227. token.id = token.key
  228. for (let ii = minLength; ii < token.key.length; ++ii) {
  229. const match = tokens.some(
  230. ot => ot !== token && ot.key.slice(0, ii) === token.key.slice(0, ii)
  231. )
  232. if (!match) {
  233. token.id = token.key.slice(0, ii)
  234. break
  235. }
  236. }
  237. }
  238. }
  239. async validateCIDRList (cidrs) {
  240. const { v4: isCidrV4, v6: isCidrV6 } = await import('is-cidr')
  241. const maybeList = [].concat(cidrs).filter(Boolean)
  242. const list = maybeList.length === 1 ? maybeList[0].split(/,\s*/) : maybeList
  243. for (const cidr of list) {
  244. if (isCidrV6(cidr)) {
  245. throw this.invalidCIDRError(
  246. `CIDR whitelist can only contain IPv4 addresses, ${cidr} is IPv6`
  247. )
  248. }
  249. if (!isCidrV4(cidr)) {
  250. throw this.invalidCIDRError(`CIDR whitelist contains invalid CIDR entry: ${cidr}`)
  251. }
  252. }
  253. return list
  254. }
  255. }
  256. module.exports = Token