Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: clean up owner command and otplease #4528

Merged
merged 1 commit into from
Mar 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 56 additions & 113 deletions lib/commands/owner.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,60 +59,39 @@ class Owner extends BaseCommand {
}

async exec ([action, ...args]) {
const opts = {
...this.npm.flatOptions,
}
switch (action) {
case 'ls':
case 'list':
return this.ls(args[0], opts)
return this.ls(args[0])
case 'add':
return this.add(args[0], args[1], opts)
return this.changeOwners(args[0], args[1], 'add')
case 'rm':
case 'remove':
return this.rm(args[0], args[1], opts)
return this.changeOwners(args[0], args[1], 'rm')
default:
throw this.usageError()
}
}

async ls (pkg, opts) {
if (!pkg) {
if (this.npm.config.get('global')) {
throw this.usageError()
}

const pkgName = await readLocalPkgName(this.npm.prefix)
if (!pkgName) {
throw this.usageError()
}

pkg = pkgName
}

async ls (pkg) {
pkg = await this.getPkg(pkg)
const spec = npa(pkg)

try {
const packumentOpts = { ...opts, fullMetadata: true }
const packumentOpts = { ...this.npm.flatOptions, fullMetadata: true }
const { maintainers } = await pacote.packument(spec, packumentOpts)
if (!maintainers || !maintainers.length) {
this.npm.output('no admin found')
} else {
this.npm.output(maintainers.map(o => `${o.name} <${o.email}>`).join('\n'))
this.npm.output(maintainers.map(m => `${m.name} <${m.email}>`).join('\n'))
}

return maintainers
} catch (err) {
log.error('owner ls', "Couldn't get owner data", pkg)
throw err
}
}

async add (user, pkg, opts) {
if (!user) {
throw this.usageError()
}

async getPkg (pkg) {
if (!pkg) {
if (this.npm.config.get('global')) {
throw this.usageError()
Expand All @@ -122,44 +101,25 @@ class Owner extends BaseCommand {
throw this.usageError()
}

pkg = pkgName
return pkgName
}
log.verbose('owner add', '%s to %s', user, pkg)

const spec = npa(pkg)
return this.putOwners(spec, user, opts,
(newOwner, owners) => this.validateAddOwner(newOwner, owners))
return pkg
}

async rm (user, pkg, opts) {
async changeOwners (user, pkg, addOrRm) {
if (!user) {
throw this.usageError()
}

if (!pkg) {
if (this.npm.config.get('global')) {
throw this.usageError()
}
const pkgName = await readLocalPkgName(this.npm.prefix)
if (!pkgName) {
throw this.usageError()
}

pkg = pkgName
}
log.verbose('owner rm', '%s from %s', user, pkg)
pkg = await this.getPkg(pkg)
log.verbose(`owner ${addOrRm}`, '%s to %s', user, pkg)

const spec = npa(pkg)
return this.putOwners(spec, user, opts,
(rmOwner, owners) => this.validateRmOwner(rmOwner, owners))
}

async putOwners (spec, user, opts, validation) {
const uri = `/-/user/org.couchdb.user:${encodeURIComponent(user)}`
let u = ''
let u

try {
u = await npmFetch.json(uri, opts)
u = await npmFetch.json(uri, this.npm.flatOptions)
} catch (err) {
log.error('owner mutate', `Error getting user data for ${user}`)
throw err
Expand All @@ -177,36 +137,60 @@ class Owner extends BaseCommand {
// normalize user data
u = { name: u.name, email: u.email }

const data = await pacote.packument(spec, { ...opts, fullMetadata: true })
const data = await pacote.packument(spec, { ...this.npm.flatOptions, fullMetadata: true })

// save the number of maintainers before validation for comparison
const before = data.maintainers ? data.maintainers.length : 0
const owners = data.maintainers || []
let maintainers
if (addOrRm === 'add') {
const existing = owners.find(o => o.name === u.name)
if (existing) {
log.info(
'owner add',
`Already a package owner: ${existing.name} <${existing.email}>`
)
return
}
maintainers = [
...owners,
u,
]
} else {
maintainers = owners.filter(o => o.name !== u.name)

const m = validation(u, data.maintainers)
if (!m) {
return
} // invalid owners
if (maintainers.length === owners.length) {
log.info('owner rm', 'Not a package owner: ' + u.name)
return false
}

const body = {
_id: data._id,
_rev: data._rev,
maintainers: m,
if (!maintainers.length) {
throw Object.assign(
new Error(
'Cannot remove all owners of a package. Add someone else first.'
),
{ code: 'EOWNERRM' }
)
}
}

const dataPath = `/${spec.escapedName}/-rev/${encodeURIComponent(data._rev)}`
const res = await otplease(opts, opts => {
const res = await otplease(this.npm.flatOptions, opts => {
return npmFetch.json(dataPath, {
...opts,
method: 'PUT',
body,
body: {
_id: data._id,
_rev: data._rev,
maintainers,
},
spec,
})
})

if (!res.error) {
if (m.length < before) {
this.npm.output(`- ${user} (${spec.name})`)
} else {
if (addOrRm === 'add') {
this.npm.output(`+ ${user} (${spec.name})`)
} else {
this.npm.output(`- ${user} (${spec.name})`)
}
} else {
throw Object.assign(
Expand All @@ -216,47 +200,6 @@ class Owner extends BaseCommand {
}
return res
}

validateAddOwner (newOwner, owners) {
owners = owners || []
for (const o of owners) {
if (o.name === newOwner.name) {
log.info(
'owner add',
'Already a package owner: ' + o.name + ' <' + o.email + '>'
)
return false
}
}
return [
...owners,
newOwner,
]
}

validateRmOwner (rmOwner, owners) {
let found = false
const m = owners.filter(function (o) {
var match = (o.name === rmOwner.name)
found = found || match
return !match
})

if (!found) {
log.info('owner rm', 'Not a package owner: ' + rmOwner.name)
return false
}

if (!m.length) {
throw Object.assign(
new Error(
'Cannot remove all owners of a package. Add someone else first.'
),
{ code: 'EOWNERRM' }
)
}

return m
}
}

module.exports = Owner
19 changes: 8 additions & 11 deletions lib/utils/otplease.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
const prompt = 'This operation requires a one-time password.\nEnter OTP:'
const readUserInfo = require('./read-user-info.js')

const isOtpError = err =>
err.code === 'EOTP' || (err.code === 'E401' && /one-time pass/.test(err.body))

module.exports = otplease
function otplease (opts, fn) {
opts = { prompt, ...opts }
return Promise.resolve().then(() => fn(opts)).catch(err => {
if (!isOtpError(err)) {
async function otplease (opts, fn) {
try {
await fn(opts)
} catch (err) {
if (err.code !== 'EOTP' && (err.code !== 'E401' || !/one-time pass/.test(err.body))) {
throw err
} else if (!process.stdin.isTTY || !process.stdout.isTTY) {
throw err
} else {
return readUserInfo.otp(opts.prompt)
.then(otp => fn({ ...opts, otp }))
const otp = await readUserInfo.otp('This operation requires a one-time password.\nEnter OTP:')
return fn({ ...opts, otp })
}
})
}
}