access.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. const libnpmaccess = require('libnpmaccess')
  2. const npa = require('npm-package-arg')
  3. const { output } = require('proc-log')
  4. const pkgJson = require('@npmcli/package-json')
  5. const localeCompare = require('@isaacs/string-locale-compare')('en')
  6. const { otplease } = require('../utils/auth.js')
  7. const getIdentity = require('../utils/get-identity.js')
  8. const BaseCommand = require('../base-cmd.js')
  9. const commands = [
  10. 'get',
  11. 'grant',
  12. 'list',
  13. 'revoke',
  14. 'set',
  15. ]
  16. const setCommands = [
  17. 'status=public',
  18. 'status=private',
  19. 'mfa=none',
  20. 'mfa=publish',
  21. 'mfa=automation',
  22. '2fa=none',
  23. '2fa=publish',
  24. '2fa=automation',
  25. ]
  26. class Access extends BaseCommand {
  27. static description = 'Set access level on published packages'
  28. static name = 'access'
  29. static params = [
  30. 'json',
  31. 'otp',
  32. 'registry',
  33. ]
  34. static usage = [
  35. 'list packages [<user>|<scope>|<scope:team>] [<package>]',
  36. 'list collaborators [<package> [<user>]]',
  37. 'get status [<package>]',
  38. 'set status=public|private [<package>]',
  39. 'set mfa=none|publish|automation [<package>]',
  40. 'grant <read-only|read-write> <scope:team> [<package>]',
  41. 'revoke <scope:team> [<package>]',
  42. ]
  43. static async completion (opts) {
  44. const argv = opts.conf.argv.remain
  45. if (argv.length === 2) {
  46. return commands
  47. }
  48. if (argv.length === 3) {
  49. switch (argv[2]) {
  50. case 'grant':
  51. return ['read-only', 'read-write']
  52. case 'revoke':
  53. return []
  54. case 'list':
  55. case 'ls':
  56. return ['packages', 'collaborators']
  57. case 'get':
  58. return ['status']
  59. case 'set':
  60. return setCommands
  61. default:
  62. throw new Error(argv[2] + ' not recognized')
  63. }
  64. }
  65. }
  66. async exec ([cmd, subcmd, ...args]) {
  67. if (!cmd) {
  68. throw this.usageError()
  69. }
  70. if (!commands.includes(cmd)) {
  71. throw this.usageError(`${cmd} is not a valid access command`)
  72. }
  73. // All commands take at least one more parameter so we can do this check up front
  74. if (!subcmd) {
  75. throw this.usageError()
  76. }
  77. switch (cmd) {
  78. case 'grant':
  79. if (!['read-only', 'read-write'].includes(subcmd)) {
  80. throw this.usageError('grant must be either `read-only` or `read-write`')
  81. }
  82. if (!args[0]) {
  83. throw this.usageError('`<scope:team>` argument is required')
  84. }
  85. return this.#grant(subcmd, args[0], args[1])
  86. case 'revoke':
  87. return this.#revoke(subcmd, args[0])
  88. case 'list':
  89. case 'ls':
  90. if (subcmd === 'packages') {
  91. return this.#listPackages(args[0], args[1])
  92. }
  93. if (subcmd === 'collaborators') {
  94. return this.#listCollaborators(args[0], args[1])
  95. }
  96. throw this.usageError(`list ${subcmd} is not a valid access command`)
  97. case 'get':
  98. if (subcmd !== 'status') {
  99. throw this.usageError(`get ${subcmd} is not a valid access command`)
  100. }
  101. return this.#getStatus(args[0])
  102. case 'set':
  103. if (!setCommands.includes(subcmd)) {
  104. throw this.usageError(`set ${subcmd} is not a valid access command`)
  105. }
  106. return this.#set(subcmd, args[0])
  107. }
  108. }
  109. async #grant (permissions, scope, pkg) {
  110. await otplease(this.npm, this.npm.flatOptions, async (opts) => {
  111. await libnpmaccess.setPermissions(scope, pkg, permissions, opts)
  112. })
  113. }
  114. async #revoke (scope, pkg) {
  115. await otplease(this.npm, this.npm.flatOptions, async (opts) => {
  116. await libnpmaccess.removePermissions(scope, pkg, opts)
  117. })
  118. }
  119. async #listPackages (owner, pkg) {
  120. if (!owner) {
  121. owner = await getIdentity(this.npm, this.npm.flatOptions)
  122. }
  123. const pkgs = await libnpmaccess.getPackages(owner, this.npm.flatOptions)
  124. this.#output(pkgs, pkg)
  125. }
  126. async #listCollaborators (pkg, user) {
  127. const pkgName = await this.#getPackage(pkg, false)
  128. const collabs = await libnpmaccess.getCollaborators(pkgName, this.npm.flatOptions)
  129. this.#output(collabs, user)
  130. }
  131. async #getStatus (pkg) {
  132. const pkgName = await this.#getPackage(pkg, false)
  133. const visibility = await libnpmaccess.getVisibility(pkgName, this.npm.flatOptions)
  134. this.#output({ [pkgName]: visibility.public ? 'public' : 'private' })
  135. }
  136. async #set (subcmd, pkg) {
  137. const [subkey, subval] = subcmd.split('=')
  138. switch (subkey) {
  139. case 'mfa':
  140. case '2fa':
  141. return this.#setMfa(pkg, subval)
  142. case 'status':
  143. return this.#setStatus(pkg, subval)
  144. }
  145. }
  146. async #setMfa (pkg, level) {
  147. const pkgName = await this.#getPackage(pkg, false)
  148. await otplease(this.npm, this.npm.flatOptions, (opts) => {
  149. return libnpmaccess.setMfa(pkgName, level, opts)
  150. })
  151. }
  152. async #setStatus (pkg, status) {
  153. // only scoped packages can have their access changed
  154. const pkgName = await this.#getPackage(pkg, true)
  155. if (status === 'private') {
  156. status = 'restricted'
  157. }
  158. await otplease(this.npm, this.npm.flatOptions, (opts) => {
  159. return libnpmaccess.setAccess(pkgName, status, opts)
  160. })
  161. return this.#getStatus(pkgName)
  162. }
  163. async #getPackage (name, requireScope) {
  164. if (!name) {
  165. try {
  166. const { content } = await pkgJson.normalize(this.npm.prefix)
  167. name = content.name
  168. } catch (err) {
  169. if (err.code === 'ENOENT') {
  170. throw Object.assign(new Error('no package name given and no package.json found'), {
  171. code: 'ENOENT',
  172. })
  173. } else {
  174. throw err
  175. }
  176. }
  177. }
  178. const spec = npa(name)
  179. if (requireScope && !spec.scope) {
  180. throw this.usageError('This command is only available for scoped packages.')
  181. }
  182. return name
  183. }
  184. #output (items, limiter) {
  185. const outputs = {}
  186. const lookup = {
  187. __proto__: null,
  188. read: 'read-only',
  189. write: 'read-write',
  190. }
  191. for (const item in items) {
  192. const val = items[item]
  193. outputs[item] = lookup[val] || val
  194. }
  195. if (this.npm.config.get('json')) {
  196. output.buffer(outputs)
  197. } else {
  198. for (const item of Object.keys(outputs).sort(localeCompare)) {
  199. if (!limiter || limiter === item) {
  200. output.standard(`${item}: ${outputs[item]}`)
  201. }
  202. }
  203. }
  204. }
  205. }
  206. module.exports = Access