diff --git a/CHANGES.md b/CHANGES.md index db02aa1..bb0999a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ house RBAC CLI functionality. - `triton rbac users` to list all users. - `triton rbac user ...` to show, create, edit and delete users. + - `triton rbac roles` to list all roles. + - `triton rbac role ...` to show, create, edit and delete roles. ## 2.1.4 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 4fbc36f..ae4cdef 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -713,7 +713,6 @@ CloudApi.prototype.getUser = function getUser(opts, cb) { /** * * - * @param {String} account (optional) the login name of the account. * @param {Object} opts (object) user object containing: * - {String} login (required) for your user. * - {String} password (required) for the user. @@ -828,6 +827,127 @@ CloudApi.prototype.deleteUser = function deleteUser(opts, cb) { }; +/** + * + * + * @param opts {Object} Options (optional) + * @param cb {Function} Callback of the form `function (err, roles, res)` + */ +CloudApi.prototype.listRoles = function listRoles(opts, cb) { + if (cb === undefined) { + cb = opts; + opts = {}; + } + assert.func(cb, 'cb'); + assert.object(opts, 'opts'); + + var endpoint = format('/%s/roles', this.account); + this._passThrough(endpoint, opts, cb); +}; + +/** + * + * + * @param {Object} opts + * - id {UUID|String} The role ID or name. + * @param {Function} callback of the form `function (err, user, res)` + */ +CloudApi.prototype.getRole = function getRole(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/roles/%s', this.account, opts.id); + this._passThrough(endpoint, {}, cb); +}; + +/** + * + * + * @param {Object} opts (object) role object containing: + * - {String} name (required) for the role. + * - {Array} members (optional) for the role. + * - {Array} default_members (optional) for the role. + * - {Array} policies (optional) for the role. + * @param {Function} cb of the form `function (err, role, res)` + */ +CloudApi.prototype.createRole = function createRole(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.name, 'opts.name'); + // XXX strict on inputs + assert.func(cb, 'cb'); + + var data = { + name: opts.name, + default_members: opts.default_members, + members: opts.members, + policies: opts.policies + }; + + this._request({ + method: 'POST', + path: format('/%s/roles', this.account), + data: data + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + +/** + * + * + * @param {Object} opts (object) role object containing: + * - {UUID|String} id (required) The role ID or name. + * - {String} name (optional) for the role. + * - {Array} members (optional) for the role. + * - {Array} default_members (optional) for the role. + * - {Array} policies (optional) for the role. + * @param {Function} cb of the form `function (err, role, res)` + */ +CloudApi.prototype.updateRole = function updateRole(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + // XXX strict on inputs + assert.func(cb, 'cb'); + + var update = { + name: opts.name, + members: opts.members, + default_members: opts.default_members, + policies: opts.policies + }; + + this._request({ + method: 'POST', + path: format('/%s/roles/%s', this.account, opts.id), + data: update + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * + * + * @param {Object} opts (object) user object containing: + * - {String} id (required) of the role to delete. + * @param {Function} cb of the form `function (err, user, res)` + */ +CloudApi.prototype.deleteRole = function deleteRole(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/roles/%s', this.account, opts.id) + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + // --- Exports diff --git a/lib/do_rbac/do_role.js b/lib/do_rbac/do_role.js new file mode 100644 index 0000000..89e56af --- /dev/null +++ b/lib/do_rbac/do_role.js @@ -0,0 +1,518 @@ +/* + * 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 role ...` + */ + +var assert = require('assert-plus'); +var format = require('util').format; +var fs = require('fs'); +var strsplit = require('strsplit'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + + +var UPDATABLE_ROLE_FIELDS = [ + {key: 'name', required: true}, + {key: 'default_members', array: true}, + {key: 'members', array: true}, + {key: 'policies', array: true} +]; + +var CREATE_ROLE_FIELDS = [ + {key: 'name', required: true}, + {key: 'default_members', array: true}, + {key: 'members', array: true}, + {key: 'policies', array: true} +]; + +var _isArrayFromKey = {}; +UPDATABLE_ROLE_FIELDS.forEach(function (field) { + _isArrayFromKey[field.key] = Boolean(field.array); +}); + + +function _arrayFromCSV(csv) { + // JSSTYLED + return csv.split(/\s*,\s*/g).filter(function (v) { return v; }); +} + + +function _showRole(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.id, 'opts.id'); + assert.optionalBool(opts.roles, 'opts.roles'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + cli.tritonapi.getRole({ + id: opts.id, + roles: opts.roles + }, function onRole(err, role) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(role)); + } else { + Object.keys(role).forEach(function (key) { + var val = role[key]; + if (Array.isArray(val)) { + val = val.join(', '); + } + console.log('%s: %s', key, val); + }); + } + cb(); + }); +} + +function _yamlishFromRole(role) { + assert.object(role, 'role'); + + var lines = []; + UPDATABLE_ROLE_FIELDS.forEach(function (field) { + var key = field.key; + var val = role[key]; + if (!val) { + val = ''; + } else if (Array.isArray(val)) { + val = val.join(', '); + } + lines.push(format('%s: %s', key, val)); + }); + return lines.join('\n') + '\n'; +} + +function _roleFromYamlish(yamlish) { + assert.string(yamlish, 'yamlish'); + + var role = {}; + var lines = yamlish.split(/\n/g); + lines.forEach(function (line) { + var commentIdx = line.indexOf('#'); + if (commentIdx !== -1) { + line = line.slice(0, commentIdx); + } + line = line.trim(); + if (!line) { + return; + } + var parts = strsplit(line, ':', 2); + var key = parts[0].trim(); + var value = parts[1].trim(); + if (_isArrayFromKey[key]) { + value = _arrayFromCSV(value); + } + role[key] = value; + }); + + return role; +} + + +function _editRole(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + var role; + var filename; + var origText; + + function offerRetry(afterText) { + common.promptEnter( + 'Press to re-edit, Ctrl+C to abort.', + function (aborted) { + if (aborted) { + console.log('\nAborting. No change made to role.'); + cb(); + } else { + editAttempt(afterText); + } + }); + } + + function editAttempt(text) { + common.editInEditor({ + text: text, + filename: filename + }, function (err, afterText, changed) { + if (err) { + return cb(new errors.TritonError(err)); + } + // We don't use this `changed` in case it is a second attempt. + + try { + var editedRole = _roleFromYamlish(afterText); + editedRole.id = role.id; + + if (_yamlishFromRole(editedRole) === origText) { + // This YAMLish is the closest to a canonical form we have. + console.log('No change to role'); + cb(); + return; + } + } catch (textErr) { + console.error('Error with your changes: %s', textErr); + offerRetry(afterText); + return; + } + + // Save changes. + cli.tritonapi.cloudapi.updateRole(editedRole, function (uErr, ur) { + if (uErr) { + console.error('Error updating role with your changes: %s', + uErr); + offerRetry(afterText); + return; + } + console.log('Updated role "%s" (%s)', ur.name, ur.id); + cb(); + }); + }); + } + + + cli.tritonapi.getRole({ + id: opts.id + }, function onRole(err, role_) { + if (err) { + return cb(err); + } + + role = role_; + filename = format('%s-role-%s.txt', cli.tritonapi.profile.account, + role.name); + origText = _yamlishFromRole(role); + editAttempt(origText); + }); +} + + +function _deleteRoles(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.arrayOfString(opts.ids, 'opts.ids'); + assert.optionalBool(opts.yes, 'opts.yes'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + if (opts.ids.length === 0) { + cb(); + return; + } + + vasync.pipeline({funcs: [ + function confirm(_, next) { + if (opts.yes) { + return next(); + } + var msg; + if (opts.ids.length === 1) { + msg = 'Delete role "' + opts.ids[0] + '"? [y/n] '; + } else { + msg = 'Delete %d roles (' + opts.ids.join(', ') + ')? [y/n] '; + + } + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + function deleteThem(_, next) { + vasync.forEachPipeline({ + inputs: opts.ids, + func: function deleteOne(id, nextId) { + cli.tritonapi.deleteRole({id: id}, function (err) { + if (err) { + nextId(err); + return; + } + console.log('Deleted role "%s"', id); + nextId(); + }); + } + }, next); + } + ]}, function (err) { + if (err === true) { + err = null; + } + cb(err); + }); +} + + +function _addRole(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.optionalString(opts.file, 'opts.file'); + assert.func(cb, 'cb'); + var cli = opts.cli; + var log = cli.log; + + var data; + + vasync.pipeline({funcs: [ + function gatherDataStdin(_, next) { + if (opts.file !== '-') { + return next(); + } + var stdin = ''; + process.stdin.resume(); + process.stdin.on('data', function (chunk) { + stdin += chunk; + }); + process.stdin.on('end', function () { + try { + data = JSON.parse(stdin); + } catch (err) { + log.trace({stdin: stdin}, 'invalid role JSON on stdin'); + return next(new errors.TritonError( + format('invalid role JSON on stdin: %s', err))); + } + next(); + }); + }, + function gatherDataFile(_, next) { + if (!opts.file || opts.file === '-') { + return next(); + } + var input = fs.readFileSync(opts.file); + try { + data = JSON.parse(input); + } catch (err) { + return next(new errors.TritonError(format( + 'invalid role JSON in "%s": %s', opts.file, err))); + } + next(); + }, + function gatherDataInteractive(_, next) { + if (opts.file) { + return next(); + } else if (!process.stdin.isTTY) { + return next(new errors.UsageError('cannot interactively ' + + 'create a role: stdin is not a TTY')); + } else if (!process.stdout.isTTY) { + return next(new errors.UsageError('cannot interactively ' + + 'create a role: stdout is not a TTY')); + } + + // TODO: retries on failure + // TODO: on failure write out to a tmp file with cmd to add it + data = {}; + vasync.forEachPipeline({ + inputs: CREATE_ROLE_FIELDS, + func: function getField(field_, nextField) { + var field = common.objCopy(field_); + + // 'members' needs to hold all default_members, so default + // that. + if (field.key === 'members') { + field['default'] = data['default_members'].join(', '); + } + + common.promptField(field, function (err, value) { + if (value) { + if (_isArrayFromKey[field.key]) { + value = _arrayFromCSV(value); + } + data[field.key] = value; + } + nextField(err); + }); + } + }, function (err) { + console.log(); + next(err); + }); + }, + function validateData(_, next) { + var missing = []; + var dataCopy = common.objCopy(data); + CREATE_ROLE_FIELDS.forEach(function (field) { + if (dataCopy.hasOwnProperty(field.key)) { + delete dataCopy[field.key]; + } else if (field.required) { + missing.push(field.key); + } + }); + var extra = Object.keys(dataCopy); + var issues = []; + if (missing.length) { + issues.push(format('%s missing required field%s: %s', + missing.length, (missing.length === 1 ? '' : 's'), + missing.join(', '))); + } + if (extra.length) { + issues.push(format('extraneous field%s: %s', + (extra.length === 1 ? '' : 's'), extra.join(', '))); + } + if (issues.length) { + next(new errors.TritonError( + 'invalid role data: ' + issues.join('; '))); + } else { + next(); + } + }, + function createIt(_, next) { + cli.tritonapi.cloudapi.createRole(data, function (err, role) { + if (err) { + next(err); + return; + } + console.log('Created role "%s"', role.name); + next(); + }); + } + ]}, cb); +} + + +function do_role(subcmd, opts, args, cb) { + if (opts.help) { + this.do_help('help', {}, [subcmd], cb); + return; + } + + // Which action? + var actions = []; + if (opts.add) { actions.push('add'); } + if (opts.edit) { actions.push('edit'); } + if (opts['delete']) { actions.push('delete'); } + var action; + if (actions.length === 0) { + action = 'show'; + } else if (actions.length > 1) { + return cb(new errors.UsageError( + 'only one action option may be used at once')); + } else { + action = actions[0]; + } + + // Arg count validation. + if (args.length > 1) { + return cb(new errors.UsageError('too many arguments')); + } else if (args.length === 0 && + ['show', 'edit'].indexOf(action) !== -1) + { + return cb(new errors.UsageError('ROLE argument is required')); + } + + switch (action) { + case 'show': + _showRole({ + cli: this.top, + id: args[0], + json: opts.json + }, cb); + break; + case 'edit': + _editRole({ + cli: this.top, + id: args[0] + }, cb); + break; + case 'delete': + _deleteRoles({ + cli: this.top, + ids: args, + yes: opts.yes + }, cb); + break; + case 'add': + _addRole({cli: this.top, file: args[0]}, cb); + break; + default: + return cb(new errors.InternalError('unknown action: ' + action)); + } +} + +do_role.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + }, + { + names: ['roles', 'r'], + type: 'bool', + help: 'Include "roles" and "default_roles" this user has.' + }, + { + names: ['membership'], + type: 'bool', + help: 'Include "roles" and "default_roles" this user has. Included ' + + 'for backward compat with `sdc-user get --membership ...` from ' + + 'node-smartdc.', + hidden: true + }, + { + names: ['yes', 'y'], + type: 'bool', + help: 'Answer yes to confirmations, e.g. confirmation of deletion.' + }, + { + group: 'Action Options' + }, + { + names: ['edit', 'e'], + type: 'bool', + help: 'Edit the named user in your $EDITOR.' + }, + { + names: ['add', 'a'], + type: 'bool', + help: 'Add a new user.' + }, + { + names: ['delete', 'd'], + type: 'bool', + help: 'Delete the named user.' + } +]; +do_role.help = [ + /* BEGIN JSSTYLED */ + 'Show, add, edit and delete RBAC roles.', + '', + 'Usage:', + ' {{name}} role ROLE # show role ROLE', + ' {{name}} role -e|--edit ROLE # edit role ROLE in $EDITOR', + ' {{name}} role -d|--delete [ROLE...] # delete role ROLE', + '', + ' {{name}} role -a|--add [FILE]', + ' # Add a new role. FILE must be a file path to a JSON file', + ' # with the role data or "-" to pass the role in on stdin.', + ' # Or exclude FILE to interactively add.', + '', + '{{options}}', + 'Where "ROLE" is a full role "id", the role "login" name or a "shortid", i.e.', + 'an id prefix.', + '', + 'Fields for creating a role:', + CREATE_ROLE_FIELDS.map(function (field) { + return ' ' + field.key + (field.required ? ' (required)' : ''); + }).join('\n') + /* END JSSTYLED */ +].join('\n'); + +module.exports = do_role; diff --git a/lib/do_rbac/do_roles.js b/lib/do_rbac/do_roles.js new file mode 100644 index 0000000..cdf123d --- /dev/null +++ b/lib/do_rbac/do_roles.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 roles ...` + */ + +var tabula = require('tabula'); + +var common = require('../common'); +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 sortDefault = 'name'; + + +function do_roles(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 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.listRoles(function (err, roles) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + common.jsonStream(roles); + } else { + var i, j; + // 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++) { + defaultMap[role.default_members[j]] = true; + } + role.default_members = role.default_members.sort().join(','); + var sortedRawMembers = role.members.sort(); + var defaultMembers = []; + var members = []; + for (j = 0; j < sortedRawMembers.length; j++) { + var m = sortedRawMembers[j]; + if (defaultMap[m]) { + defaultMembers.push(m); + // TODO: formal envvar with a --no-color top-level opt + } else if (process.env.TRITON_NO_COLOR) { + members.push(m); + } else { + members.push(common.ansiStylize(m, 'magenta')); + } + } + role.members = defaultMembers.concat(members).join(','); + } + + tabula(roles, { + skipHeader: opts.H, + columns: columns, + sort: sort + }); + } + cb(); + }); +} + +do_roles.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +].concat(common.getCliTableOptions({ + includeLong: true, + sortDefault: sortDefault +})); + +do_roles.help = ( + /* BEGIN JSSTYLED */ + 'List RBAC roles.\n' + + '\n' + + 'Usage:\n' + + ' {{name}} roles []\n' + + '\n' + + '{{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 */ +); + + + +module.exports = do_roles; diff --git a/lib/do_rbac/do_user.js b/lib/do_rbac/do_user.js index c4d6fbb..1a86689 100644 --- a/lib/do_rbac/do_user.js +++ b/lib/do_rbac/do_user.js @@ -182,7 +182,8 @@ function _editUser(opts, cb) { } user = user_; - filename = format('user-%s-%s.txt', cli.account, user.login); + filename = format('%s-user-%s.txt', cli.tritonapi.profile.account, + user.login); origText = _yamlishFromUser(user); editAttempt(origText); }); diff --git a/lib/do_rbac/index.js b/lib/do_rbac/index.js index 31b4d79..2e3a511 100644 --- a/lib/do_rbac/index.js +++ b/lib/do_rbac/index.js @@ -44,4 +44,7 @@ RbacCLI.prototype.init = function init(opts, args, cb) { RbacCLI.prototype.do_users = require('./do_users'); RbacCLI.prototype.do_user = require('./do_user'); +RbacCLI.prototype.do_roles = require('./do_roles'); +RbacCLI.prototype.do_role = require('./do_role'); + module.exports = RbacCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 62741de..b677898 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -681,7 +681,7 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { 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))); + 'user with login or id "%s" was not found', opts.id))); return; } else if (!opts.roles || ctx.user.roles) { next(); @@ -696,7 +696,7 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { if (err) { if (err.restCode === 'ResourceNotFound') { next(new errors.ResourceNotFoundError(err, format( - 'user with id %s was not found', opts.id))); + 'user with id "%s" was not found', opts.id))); } else { next(err); } @@ -714,6 +714,137 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { +/** + * Get an RBAC role by ID, name, or short ID, in that order. + * + * @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. + * @param {Function} callback of the form `function (err, user)` + */ +TritonApi.prototype.deleteRole = function deleteRole(opts, cb) { + var self = this; + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + /* + * CloudAPI DeleteRole only accepts a role id (UUID). + */ + var context = {}; + vasync.pipeline({arg: context, funcs: [ + function getId(ctx, next) { + if (common.isUUID(opts.id)) { + ctx.id = opts.id; + next(); + return; + } + + self.getRole({id: opts.id}, function (err, role) { + ctx.id = role.id; + next(err); + }); + }, + + function deleteIt(ctx, next) { + self.cloudapi.deleteRole({id: ctx.id}, next); + } + ]}, function (err) { + cb(err); + }); +}; + //---- exports diff --git a/package.json b/package.json index 8399a90..b355334 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "sshpk": "1.4.4", "smartdc-auth": "2.1.7", "strsplit": "1.0.0", - "tabula": "1.6.1", + "tabula": "1.7.0", "tilde-expansion": "0.0.0", "vasync": "1.6.3", "verror": "1.6.0",