token.js 6.7 KB

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