diff --git a/CHANGES.md b/CHANGES.md index 71aaf8c..f8a0522 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,8 +2,23 @@ ## 3.0.1 (not yet released) -- #54 `triton rbac info` improvements: better help, use brackets to show - non-default roles. +- #54 RBAC support, see to start. + - `triton rbac info` improvements: better help, use brackets to show + non-default roles. + - `triton rbac reset` + - change `triton rbac user USER` output a little for the 'keys' (show + the key fingerprint and name instead of the key content), 'roles', + and 'default_roles' fields. +- #54 *Drop* support for shortIds for `triton rbac {users,roles,policies}` + commands. They all have unique *`name`* fields, just use that. +- #54 `triton rbac apply` will implicitly look for a user key file at + "./rbac-user-keys/$login.pub" if no `keys` field is provided in the + "rbac.json" config file. +- Change default `triton keys` and `triton rbac keys` output to be tabular. + Otherwise it is a little obtuse to see fingerprints (which is what currently + must be included in a profile). `triton [rbac] keys -A` can be used to + get the old behaviour (just the key content, i.e. output appropriate + for "~/.ssh/authorized\_keys"). ## 3.0.0 @@ -24,7 +39,7 @@ - `triton rbac policy ...` to show, create, edit and delete policies. - `triton rbac keys` to list all RBAC user SSH keys. - `triton rbac key ...` to show, create, edit and delete user keys. - - `triton rbac {instance,image,network,package,}role-tags ...` to list + - `triton rbac {instance,image,network,package,}role-tags ...` to list and manage role tags on each of those resources. - `triton rbac info` will dump a summary of the full current RBAC state. This command is still in development. diff --git a/examples/rbac-simple/rbac.json b/examples/rbac-simple/rbac.json index 6d9ccd0..6355234 100644 --- a/examples/rbac-simple/rbac.json +++ b/examples/rbac-simple/rbac.json @@ -1,6 +1,6 @@ { "users": [ - { "login": "emma", "email": "emma@simple.example.com", "keys": "rbac-user-keys" }, + { "login": "emma", "email": "emma@simple.example.com" }, { "login": "bert", "email": "bert@simple.example.com" } ], "roles": [ diff --git a/lib/common.js b/lib/common.js index eb8dfce..91f1854 100644 --- a/lib/common.js +++ b/lib/common.js @@ -734,6 +734,12 @@ function chomp(s) { * using ASCII printable, non-space chars: ASCII 33-126 (inclusive). * * No idea if this is crypto-sound. Doubt it. + * + * This needs to pass UFDS's "insufficientPasswordQuality". This actually + * depends on the pwdcheckquality configurable JS function configured for + * the datacenter. By default that is from: + * https://github.com/joyent/sdc-ufds/blob/master/data/bootstrap.ldif.in + * which at the time of writing requires at least a number and a letter. */ function generatePassword(opts) { assert.optionalObject(opts, 'opts'); @@ -748,8 +754,15 @@ function generatePassword(opts) { var num = Math.round(((buf[i] / 0xff) * (max - min)) + min); chars.push(String.fromCharCode(num)); } + var pwd = chars.join(''); - return chars.join(''); + // "quality" checks + if (!/[a-zA-Z]+/.test(pwd) || !/[0-9]+/.test(pwd)) { + // Try again. + return generatePassword(opts); + } else { + return pwd; + } } diff --git a/lib/do_rbac/do_info.js b/lib/do_rbac/do_info.js index f63cd6b..c9f6faf 100644 --- a/lib/do_rbac/do_info.js +++ b/lib/do_rbac/do_info.js @@ -60,8 +60,6 @@ var common = require('../common'); var errors = require('../errors'); var rbac = require('../rbac'); -var ansiStylize = common.ansiStylize; - function do_info(subcmd, opts, args, cb) { @@ -74,6 +72,11 @@ function do_info(subcmd, opts, args, cb) { } var log = this.log; + var ansiStylize = common.ansiStylize; + if (!process.stdout.isTTY || opts.no_color) { + ansiStylize = function (s) { return s; }; + } + var context = { log: this.log, tritonapi: this.top.tritonapi, @@ -200,6 +203,11 @@ do_info.options = [ type: 'bool', help: 'Include all info for a more full report. This requires more ' + 'work to gather all info.' + }, + { + names: ['no-color'], + type: 'bool', + help: 'Do not color some of the output with ANSI codes.' } ]; diff --git a/lib/do_rbac/do_keys.js b/lib/do_rbac/do_keys.js index 0723d56..11682ca 100644 --- a/lib/do_rbac/do_keys.js +++ b/lib/do_rbac/do_keys.js @@ -10,10 +10,16 @@ * `triton rbac keys ...` */ +var tabula = require('tabula'); + var common = require('../common'); var errors = require('../errors'); +var columnsDefault = 'fingerprint,name'; +var columnsDefaultLong = 'fingerprint,name,key'; +var sortDefault = 'name'; + function do_keys(subcmd, opts, args, cb) { if (opts.help) { @@ -27,6 +33,15 @@ function do_keys(subcmd, opts, args, cb) { return; } + var columns = columnsDefault; + if (opts.o) { + columns = opts.o; + } else if (opts.long) { + columns = columnsDefaultLong; + } + columns = columns.split(','); + var sort = opts.s.split(','); + this.top.tritonapi.cloudapi.listUserKeys({userId: args[0]}, function (err, userKeys) { if (err) { @@ -36,10 +51,16 @@ function do_keys(subcmd, opts, args, cb) { if (opts.json) { common.jsonStream(userKeys); - } else { + } else if (opts.authorized_keys) { userKeys.forEach(function (key) { console.log(common.chomp(key.key)); }); + } else { + tabula(userKeys, { + skipHeader: false, + columns: columns, + sort: sort + }); } cb(); }); @@ -50,13 +71,18 @@ do_keys.options = [ names: ['help', 'h'], type: 'bool', help: 'Show this help.' - }, - { - names: ['json', 'j'], - type: 'bool', - help: 'JSON output.' } -]; +].concat(common.getCliTableOptions({ + includeLong: true, + sortDefault: sortDefault +})).concat([ + { + names: ['authorized-keys', 'A'], + type: 'bool', + help: 'Just output public key data -- i.e. output appropriate for a ' + + '"~/.ssh/authorized_keys" file.' + } +]); do_keys.help = ( /* BEGIN JSSTYLED */ @@ -67,10 +93,7 @@ do_keys.help = ( '\n' + '{{options}}' + '\n' + - 'Where "USER" is an RBAC user id, login or short id. By default this\n' + - 'lists just the key content for each key -- in other words, content\n' + - 'appropriate for a "~/.ssh/authorized_keys" file.\n' + - 'Use `{{name}} keys -j USER` to see all fields.\n' + 'Where "USER" is an RBAC user login or id (a UUID).\n' /* END JSSTYLED */ ); diff --git a/lib/do_rbac/do_policies.js b/lib/do_rbac/do_policies.js index f6e305e..4bb4af7 100644 --- a/lib/do_rbac/do_policies.js +++ b/lib/do_rbac/do_policies.js @@ -18,10 +18,10 @@ var errors = require('../errors'); // columns default without -o -var columnsDefault = 'shortid,name,description,nrules'; +var columnsDefault = 'name,description,nrules'; // columns default with -l -var columnsDefaultLong = 'id,name,rules'; +var columnsDefaultLong = 'id,name,description,rules'; // sort default with -s var sortDefault = 'name'; @@ -43,7 +43,6 @@ function do_policies(subcmd, opts, args, cb) { columns = columnsDefaultLong; } columns = columns.split(','); - var sort = opts.s.split(','); this.top.tritonapi.cloudapi.listPolicies(function (err, policies) { @@ -58,7 +57,6 @@ function do_policies(subcmd, opts, args, cb) { // Add some convenience fields for (var i = 0; i < policies.length; i++) { var role = policies[i]; - role.shortid = role.id.split('-', 1)[0]; role.nrules = role.rules.length; role.rules = role.rules.sort().join('; '); } @@ -94,7 +92,6 @@ do_policies.help = ( '{{options}}' + '\n' + 'Fields (most are self explanatory, the client adds some for convenience):\n' + - ' shortid A short ID prefix.\n' + ' nrules The number of rules in this policy.\n' /* END JSSTYLED */ ); diff --git a/lib/do_rbac/do_reset.js b/lib/do_rbac/do_reset.js new file mode 100644 index 0000000..6037cea --- /dev/null +++ b/lib/do_rbac/do_reset.js @@ -0,0 +1,125 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* + * Copyright 2015 Joyent, Inc. + * + * `triton rbac reset` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); +var rbac = require('../rbac'); + +var ansiStylize = common.ansiStylize; + + +function do_reset(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } else if (args.length !== 0) { + cb(new errors.UsageError('invalid args: ' + args)); + return; + } + + var context = { + log: this.log, + tritonapi: this.top.tritonapi, + cloudapi: this.top.tritonapi.cloudapi, + rbacDryRun: opts.dry_run + }; + vasync.pipeline({arg: context, funcs: [ + function emptyConfig(ctx, next) { + ctx.rbacConfig = {}; + next(); + }, + rbac.loadRbacState, + rbac.createRbacUpdatePlan, + function confirmApply(ctx, next) { + if (opts.yes || ctx.rbacUpdatePlan.length === 0) { + next(); + return; + } + ctx.log.info({rbacUpdatePlan: ctx.rbacUpdatePlan}, + 'rbacUpdatePlan'); + var p = console.log; + p(''); + p('This will make the following RBAC config changes:'); + ctx.rbacUpdatePlan.forEach(function (c) { + var extra = ''; + if (c.action === 'update') { + extra = format(' (%s)', + Object.keys(c.diff).map(function (field) { + return c.diff[field] + ' ' + field; + }).join(', ')); + } + p(' %s %s %s%s', + {create: 'Create', 'delete': 'Delete', + update: 'Update'}[c.action], + c.desc || c.type, + c.id, + extra); + }); + p(''); + var msg = format('Would you like to continue%s? [y/N] ', + opts.dry_run ? ' (dry-run)' : ''); + common.promptYesNo({msg: msg, default: 'n'}, function (answer) { + if (answer !== 'y') { + p('Aborting update'); + return cb(); + } + p(''); + next(); + }); + }, + rbac.executeRbacUpdatePlan + ]}, function (err) { + cb(err); + }); +} + +do_reset.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['dry-run', 'n'], + type: 'bool', + help: 'Go through the motions without applying changes.' + }, + { + names: ['yes', 'y'], + type: 'bool', + help: 'Answer "yes" to confirmation.' + } +]; + +do_reset.help = [ + /* BEGIN JSSTYLED */ + 'Reset RBAC state for this account.', + '**Warning: This will delete all RBAC info for this account.**', + '', + 'Usage:', + ' {{name}} reset []', + '', + '{{options}}', + 'Warning: Currently, RBAC state updates can take a few seconds to appear', + 'as they are replicated across data centers. This can result in unexpected', + 'no-op updates with consecutive quick re-runs of this command.' + /* END JSSTYLED */ +].join('\n'); + +do_reset.hidden = true; + + +module.exports = do_reset; diff --git a/lib/do_rbac/do_role.js b/lib/do_rbac/do_role.js index 288cd0f..a08e9ff 100644 --- a/lib/do_rbac/do_role.js +++ b/lib/do_rbac/do_role.js @@ -52,9 +52,7 @@ function _showRole(opts, cb) { assert.func(cb, 'cb'); var cli = opts.cli; - cli.tritonapi.getRole({ - id: opts.id - }, function onRole(err, role) { + cli.cloudapi.getRole({id: opts.id}, function onRole(err, role) { if (err) { cb(err); return; @@ -183,9 +181,7 @@ function _editRole(opts, cb) { } - cli.tritonapi.getRole({ - id: opts.id - }, function onRole(err, role_) { + cli.cloudapi.getRole({id: opts.id}, function onRole(err, role_) { if (err) { return cb(err); } diff --git a/lib/do_rbac/do_roles.js b/lib/do_rbac/do_roles.js index cdf123d..7de0845 100644 --- a/lib/do_rbac/do_roles.js +++ b/lib/do_rbac/do_roles.js @@ -17,13 +17,8 @@ var errors = require('../errors'); -// columns default without -o -var columnsDefault = 'shortid,name,policies,members'; - -// columns default with -l -var columnsDefaultLong = 'shortid,name,policies,members,default_members'; - -// sort default with -s +var columnsDefault = 'name,policies,members,default_members'; +var columnsDefaultLong = 'id,name,policies,members,default_members'; var sortDefault = 'name'; @@ -43,7 +38,6 @@ function do_roles(subcmd, opts, args, cb) { columns = columnsDefaultLong; } columns = columns.split(','); - var sort = opts.s.split(','); this.top.tritonapi.cloudapi.listRoles(function (err, roles) { @@ -59,7 +53,6 @@ function do_roles(subcmd, opts, args, cb) { // Add some convenience fields for (i = 0; i < roles.length; i++) { var role = roles[i]; - role.shortid = role.id.split('-', 1)[0]; role.policies = role.policies.sort().join(','); var defaultMap = {}; for (j = 0; j < role.default_members.length; j++) { @@ -114,7 +107,6 @@ do_roles.help = ( '{{options}}' + '\n' + 'Fields (most are self explanatory, the client adds some for convenience):\n' + - ' shortid A short ID prefix.\n' + ' members Non-default members (not in the "default_members")\n' + ' are shown in magenta.\n' /* END JSSTYLED */ diff --git a/lib/do_rbac/do_user.js b/lib/do_rbac/do_user.js index 4b55df9..2f5083e 100644 --- a/lib/do_rbac/do_user.js +++ b/lib/do_rbac/do_user.js @@ -69,19 +69,26 @@ function _showUser(opts, cb) { if (opts.json) { console.log(JSON.stringify(user)); } else { - Object.keys(user).forEach(function (key) { - if (key === 'keys') { + var skip = {keys: true, roles: true, default_roles: true}; + Object.keys(user).forEach(function (field) { + if (skip[field]) { return; } - console.log('%s: %s', key, user[key]); + console.log('%s: %s', field, user[field]); }); + if (opts.roles) { + console.log('roles:'); + user.roles.forEach( + function (r) { console.log(' ' + r); }); + console.log('default_roles:'); + user.default_roles.forEach( + function (r) { console.log(' ' + r); }); + } if (opts.keys) { console.log('keys:'); user.keys.forEach(function (key) { - process.stdout.write(' ' + key.key); - if (key.key && key.key.slice(-1) !== '\n') { - process.stdout.write('\n'); - } + console.log(' %s%s', key.fingerprint, + key.name ? format(' (%s)', key.name) : ''); }); } } @@ -495,7 +502,7 @@ do_user.help = [ 'Show, add, edit and delete RBAC users.', '', 'Usage:', - ' {{name}} user USER # show user USER', + ' {{name}} user [] USER # show user USER', ' {{name}} user -e|--edit USER # edit user USER in $EDITOR', ' {{name}} user -d|--delete [USER...] # delete user USER', '', diff --git a/lib/do_rbac/do_users.js b/lib/do_rbac/do_users.js index cb6e74e..f9c338f 100644 --- a/lib/do_rbac/do_users.js +++ b/lib/do_rbac/do_users.js @@ -17,13 +17,8 @@ var errors = require('../errors'); -// columns default without -o -var columnsDefault = 'shortid,login,email,name,cdate'; - -// columns default with -l +var columnsDefault = 'login,email,name,cdate'; var columnsDefaultLong = 'id,login,email,firstName,lastName,created'; - -// sort default with -s var sortDefault = 'login'; @@ -58,7 +53,6 @@ function do_users(subcmd, opts, args, cb) { // Add some convenience fields for (var i = 0; i < users.length; i++) { var user = users[i]; - user.shortid = user.id.split('-', 1)[0]; user.name = ((user.firstName || '') + ' ' + (user.lastName || '')).trim() || undefined; if (user.created) { @@ -98,7 +92,6 @@ do_users.help = ( ' {{name}} users []\n' + '\n' + 'Fields (most are self explanatory, the client adds some for convenience):\n' + - ' shortid A short ID prefix.\n' + ' name "firstName lastName"\n' + ' cdate Just the date portion of "created"\n' + ' udate Just the date portion of "updated"\n' + diff --git a/lib/do_rbac/index.js b/lib/do_rbac/index.js index e37769c..5a8a1fa 100644 --- a/lib/do_rbac/index.js +++ b/lib/do_rbac/index.js @@ -63,6 +63,7 @@ RbacCLI.prototype.init = function init(opts, args, cb) { RbacCLI.prototype.do_info = require('./do_info'); RbacCLI.prototype.do_apply = require('./do_apply'); +RbacCLI.prototype.do_reset = require('./do_reset'); RbacCLI.prototype.do_users = require('./do_users'); RbacCLI.prototype.do_user = require('./do_user'); diff --git a/lib/rbac.js b/lib/rbac.js index 7994b4b..5fc50f9 100644 --- a/lib/rbac.js +++ b/lib/rbac.js @@ -21,6 +21,12 @@ var common = require('./common'); var errors = require('./errors'); +// ---- globals + +var DEFAULT_RBAC_USER_KEYS_DIR = 'rbac-user-keys'; + + + // ---- internal support stuff function _rbacStateBasics(ctx, cb) { @@ -251,6 +257,11 @@ function loadRbacConfig(ctx, cb) { vasync.forEachPipeline({ inputs: ctx.rbacConfig.users || [], func: function loadForOneUser(user, nextUser) { + var implicit = false; + if (!user.hasOwnProperty('keys')) { + user.keys = DEFAULT_RBAC_USER_KEYS_DIR; + implicit = true; + } if (!user.keys || typeof (user.keys) !== 'string') { nextUser(); return; @@ -260,6 +271,10 @@ function loadRbacConfig(ctx, cb) { try { var stat = fs.statSync(keysFile); } catch (statErr) { + if (implicit) { + nextUser(); + return; + } throw new errors.TritonError(format( 'User %s keys not found in "%s": %s', user.login, keysFile, statErr)); @@ -269,12 +284,20 @@ function loadRbacConfig(ctx, cb) { try { stat = fs.statSync(keysFile); } catch (statErr) { + if (implicit) { + nextUser(); + return; + } throw new errors.TritonError(format( 'User %s keys not found in "%s": %s', user.login, keysFile, statErr)); } } if (!stat.isFile()) { + if (implicit) { + nextUser(); + return; + } throw new errors.TritonError(format( 'Expected "%s" to be a regular file', keysFile)); } @@ -564,7 +587,10 @@ function executeRbacUpdatePlan(ctx, cb) { next(err); return; } - console.log('Created user %s', c.wantThing.login); + + console.log('Created user %s (use `triton rbac passwd ' + + '%s` to change password)', c.wantThing.login, + c.wantThing.login); next(); }); break; @@ -703,7 +729,7 @@ function executeRbacUpdatePlan(ctx, cb) { next(err); return; } - console.log('Created role %s (%s members%s)', + console.log('Created role %s (%s member%s)', role.name, role.members.length, role.members.length === 1 ? '' : 's'); next(); diff --git a/lib/tritonapi.js b/lib/tritonapi.js index dd788f0..0a4b52e 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -778,10 +778,10 @@ TritonApi.prototype.setRoleTags = function setRoleTags(opts, cb) { /** - * Get an RBAC user by ID, login, or short ID, in that order. + * Get an RBAC user by ID or login. * * @param {Object} opts - * - id {UUID|String} The user ID (a UUID), login or short id. + * - id {UUID|String} The user ID (a UUID) or login. * - roles {Boolean} Optional. Whether to includes roles of which this * user is a member. Default false. * - keys {Boolean} Optional. Set to `true` to also (with a separate @@ -796,11 +796,6 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { assert.optionalBool(opts.keys, 'opts.keys'); assert.func(cb, 'cb'); - /* - * CloudAPI GetUser supports a UUID or login, so we try that first. - * If that is a 404 and `opts.id` a valid shortid, then try to lookup - * via `listUsers`. - */ var context = {}; vasync.pipeline({arg: context, funcs: [ function tryGetUser(ctx, next) { @@ -811,83 +806,11 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { self.cloudapi.getUser(getOpts, function (err, user) { if (err) { if (err.restCode === 'ResourceNotFound') { - ctx.notFoundErr = err; - next(); - } else { - next(err); - } - } else { - ctx.user = user; - next(); - } - }); - }, - - function tryShortId(ctx, next) { - if (ctx.user) { - next(); - return; - } - var shortId = common.normShortId(opts.id); - if (!shortId) { - next(); - return; - } - - self.cloudapi.listUsers(function (err, users) { - if (err) { - next(err); - return; - } - - var shortIdMatches = []; - for (var i = 0; i < users.length; i++) { - var user = users[i]; - // TODO: use this test in other shortId matching - if (user.id.slice(0, shortId.length) === shortId) { - shortIdMatches.push(user); - } - } - - if (shortIdMatches.length === 1) { - ctx.user = shortIdMatches[0]; - next(); - } else if (shortIdMatches.length === 0) { - next(new errors.ResourceNotFoundError(format( - 'user with login or id matching "%s" was not found', - opts.id))); - } else { - next(new errors.ResourceNotFoundError( - format('user with login "%s" was not found ' - + 'and "%s" is an ambiguous short id', opts.id))); - } - }); - }, - - /* - * If we found the user via `listUsers` and `opts.roles` was requested - * then we need to re-getUser. - */ - function reGetUserIfNecessary(ctx, next) { - if (!ctx.user) { - // We must have gotten the `notFoundErr` above. - next(new errors.ResourceNotFoundError(ctx.notFoundErr, format( - 'user with login or id "%s" was not found', opts.id))); - return; - } else if (!opts.roles || ctx.user.roles) { - next(); - return; - } - - var getOpts = { - id: ctx.user.id, - membership: opts.roles - }; - self.cloudapi.getUser(getOpts, function (err, user) { - if (err) { - if (err.restCode === 'ResourceNotFound') { - next(new errors.ResourceNotFoundError(err, format( - 'user with id "%s" was not found', opts.id))); + // TODO: feels like overkill to wrap this, ensure + // decent cloudapi error for this, then don't wrap. + next(new errors.ResourceNotFoundError(err, + format('user with login or id "%s" was not found', + opts.id))); } else { next(err); } @@ -920,104 +843,11 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { }; - /** - * Get an RBAC role by ID, name, or short ID, in that order. + * Delete an RBAC role by ID or name. * * @param {Object} opts - * - id {UUID|String} The RBAC role id (a UUID), name or short id. - * @param {Function} callback of the form `function (err, role)` - */ -TritonApi.prototype.getRole = function getRole(opts, cb) { - var self = this; - assert.object(opts, 'opts'); - assert.string(opts.id, 'opts.id'); - assert.func(cb, 'cb'); - - /* - * CloudAPI GetRole supports a UUID or name, so we try that first. - * If that is a 404 and `opts.id` a valid shortid, then try to lookup - * via `listRoles`. - */ - var context = {}; - vasync.pipeline({arg: context, funcs: [ - function tryGetRole(ctx, next) { - self.cloudapi.getRole({id: opts.id}, function (err, role) { - if (err) { - if (err.restCode === 'ResourceNotFound') { - ctx.notFoundErr = err; - next(); - } else { - next(err); - } - } else { - ctx.role = role; - next(); - } - }); - }, - - function tryShortId(ctx, next) { - if (ctx.role) { - next(); - return; - } - var shortId = common.normShortId(opts.id); - if (!shortId) { - next(); - return; - } - - self.cloudapi.listRoles(function (err, roles) { - if (err) { - next(err); - return; - } - - var shortIdMatches = []; - for (var i = 0; i < roles.length; i++) { - var role = roles[i]; - if (role.id.slice(0, shortId.length) === shortId) { - shortIdMatches.push(role); - } - } - - if (shortIdMatches.length === 1) { - ctx.role = shortIdMatches[0]; - next(); - } else if (shortIdMatches.length === 0) { - next(new errors.ResourceNotFoundError(format( - 'role with id or name matching "%s" was not found', - opts.id))); - } else { - next(new errors.ResourceNotFoundError( - format('role with name "%s" was not found ' - + 'and "%s" is an ambiguous short id', opts.id))); - } - }); - }, - - function raiseEarlierNotFoundErrIfNotFound(ctx, next) { - if (!ctx.role) { - // We must have gotten the `notFoundErr` above. - next(new errors.ResourceNotFoundError(ctx.notFoundErr, format( - 'role with name or id "%s" was not found', opts.id))); - } else { - next(); - } - } - ]}, function (err) { - cb(err, context.role); - }); -}; - - - -/** - * Delete an RBAC role by ID, name, or short ID, in that order. - * - * @param {Object} opts - * - id {UUID|String} The role id (a UUID), name or short id. + * - id {UUID|String} The role id (a UUID) or name. * @param {Function} callback of the form `function (err)` */ TritonApi.prototype.deleteRole = function deleteRole(opts, cb) { @@ -1038,7 +868,7 @@ TritonApi.prototype.deleteRole = function deleteRole(opts, cb) { return; } - self.getRole({id: opts.id}, function (err, role) { + self.cloudapi.getRole({id: opts.id}, function (err, role) { if (err) { next(err); return; @@ -1057,104 +887,11 @@ TritonApi.prototype.deleteRole = function deleteRole(opts, cb) { }; - /** - * Get an RBAC policy by ID, name, or short ID, in that order. + * Delete an RBAC policy by ID or name. * * @param {Object} opts - * - id {UUID|String} The RBAC policy id (a UUID), name or short id. - * @param {Function} callback of the form `function (err, policy)` - */ -TritonApi.prototype.getPolicy = function getPolicy(opts, cb) { - var self = this; - assert.object(opts, 'opts'); - assert.string(opts.id, 'opts.id'); - assert.func(cb, 'cb'); - - /* - * CloudAPI GetPolicy supports a UUID or name, so we try that first. - * If that is a 404 and `opts.id` a valid shortid, then try to lookup - * via `listPolicies`. - */ - var context = {}; - vasync.pipeline({arg: context, funcs: [ - function tryGetIt(ctx, next) { - self.cloudapi.getPolicy({id: opts.id}, function (err, policy) { - if (err) { - if (err.restCode === 'ResourceNotFound') { - ctx.notFoundErr = err; - next(); - } else { - next(err); - } - } else { - ctx.policy = policy; - next(); - } - }); - }, - - function tryShortId(ctx, next) { - if (ctx.policy) { - next(); - return; - } - var shortId = common.normShortId(opts.id); - if (!shortId) { - next(); - return; - } - - self.cloudapi.listRoles(function (err, policies) { - if (err) { - next(err); - return; - } - - var shortIdMatches = []; - for (var i = 0; i < policies.length; i++) { - var policy = policies[i]; - if (policy.id.slice(0, shortId.length) === shortId) { - shortIdMatches.push(policy); - } - } - - if (shortIdMatches.length === 1) { - ctx.policy = shortIdMatches[0]; - next(); - } else if (shortIdMatches.length === 0) { - next(new errors.ResourceNotFoundError(format( - 'policy with id or name matching "%s" was not found', - opts.id))); - } else { - next(new errors.ResourceNotFoundError( - format('policy with name "%s" was not found ' - + 'and "%s" is an ambiguous short id', opts.id))); - } - }); - }, - - function raiseEarlierNotFoundErrIfNotFound(ctx, next) { - if (!ctx.policy) { - // We must have gotten the `notFoundErr` above. - next(new errors.ResourceNotFoundError(ctx.notFoundErr, format( - 'policy with name or id "%s" was not found', opts.id))); - } else { - next(); - } - } - ]}, function (err) { - cb(err, context.policy); - }); -}; - - - -/** - * Delete an RBAC policy by ID, name, or short ID, in that order. - * - * @param {Object} opts - * - id {UUID|String} The policy id (a UUID), name or short id. + * - id {UUID|String} The policy id (a UUID) or name. * @param {Function} callback of the form `function (err)` */ TritonApi.prototype.deletePolicy = function deletePolicy(opts, cb) { @@ -1175,7 +912,7 @@ TritonApi.prototype.deletePolicy = function deletePolicy(opts, cb) { return; } - self.getPolicy({id: opts.id}, function (err, policy) { + self.cloudapi.getPolicy({id: opts.id}, function (err, policy) { if (err) { next(err); return; diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 6e9fa80..c690b48 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -82,11 +82,24 @@ var LOG = require('../lib/log'); /* * Call the `triton` CLI with the given args. + * + * @param args {String|Array} Required. CLI arguments to `triton ...` (without + * the "triton"). This can be an array of args, or a string. + * @param opts {Object} Optional. + * - opts.cwd {String} cwd option to exec. + * @param cb {Function} */ -function triton(args, cb) { +function triton(args, opts, cb) { var command = [].concat(TRITON).concat(args); if (typeof (args) === 'string') command = command.join(' '); + if (cb === undefined) { + cb = opts; + opts = {}; + } + assert.object(opts, 'opts'); + assert.optionalString(opts.cwd, 'opts.cwd'); + assert.func(cb, 'cb'); testcommon.execPlus({ command: command, @@ -101,7 +114,8 @@ function triton(args, cb) { TRITON_ACCOUNT: CONFIG.profile.account, TRITON_KEY_ID: CONFIG.profile.keyId, TRITON_TLS_INSECURE: CONFIG.profile.insecure - } + }, + cwd: opts.cwd }, log: LOG }, cb);