From 4e45e4061f99ab67ca9ca0579d89828a937afab9 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Mon, 9 Nov 2015 15:09:37 -0800 Subject: [PATCH] joyent/node-triton#54 a start at 'triton rbac info', add 'triton rbac instance-role-tags' --- CHANGES.md | 4 + lib/cloudapi2.js | 65 +++- lib/do_rbac/do_info.js | 272 +++++++++++++++++ lib/do_rbac/do_role_tags.js | 582 ++++++++++++++++++++++++++++++++++++ lib/do_rbac/index.js | 31 +- lib/tritonapi.js | 69 ++++- 6 files changed, 1011 insertions(+), 12 deletions(-) create mode 100644 lib/do_rbac/do_info.js create mode 100644 lib/do_rbac/do_role_tags.js diff --git a/CHANGES.md b/CHANGES.md index b151a62..f14b861 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -18,6 +18,10 @@ - `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 info` will dump a summary of the full current RBAC + configuration. This command is still in development. + - `triton rbac instance-role-tags ...` to list and manage role tags + on an instance. ## 2.1.4 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 92d0101..725dd76 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -224,7 +224,7 @@ CloudApi.prototype._request = function _request(options, callback) { assert.optionalObject(options.data, 'options.data'); var method = (options.method || 'GET').toLowerCase(); - assert.ok(['get', 'post', 'delete', 'head'].indexOf(method) >= 0, + assert.ok(['get', 'post', 'put', 'delete', 'head'].indexOf(method) >= 0, 'invalid method given'); switch (method) { case 'delete': @@ -1170,6 +1170,69 @@ CloudApi.prototype.deletePolicy = function deletePolicy(opts, cb) { }; +/** + * + * Set RBAC role-tags on a resource. + * + * @param {Object} opts (object): + * - {String} resource (required) The resource URL. E.g. + * '/:account/machines/:uuid' to tag a particular machine instance. + * - {Array} roleTags (required) the array of role tags to set. Each + * role tag string is the name of a RBAC role. See `ListRoles`. + * @param {Function} cb of the form `function (err, body, res)` + * Where `body` is of the form `{name: , + * 'role-tag': }`. + * @throws {AssertionError, TypeError} on invalid inputs + */ +CloudApi.prototype.setRoleTags = function setRoleTags(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.resource, 'opts.resource'); + assert.arrayOfString(opts.roleTags, 'opts.roleTags'); + assert.func(cb, 'cb'); + + // Validate `resource`. + // XXX Do we need to massage '/my' to '/:account' as old cloudapi.js does? + var resourceRe = new RegExp('^/[^/]{2,}/[^/]+'); + if (! resourceRe.test(opts.resource)) { + throw new TypeError(format('invalid resource "%s": must match ' + + '"/:account/:type..."', opts.resource)); + } + + var validResources = [ + 'machines', + 'packages', + 'images', + 'fwrules', + 'networks', + // TODO: validate/test role tags on these rbac resources + 'users', + 'roles', + 'policies', + // TODO: validate, test + 'keys', + 'datacenters', + 'analytics', + 'instrumentations' + ]; + var parts = opts.resource.split('/'); + if (validResources.indexOf(parts[2]) === -1) { + throw new TypeError(format('invalid resource "%s": resource type ' + + 'must be one of: %s', opts.resource, validResources.join(', '))); + } + + this._request({ + method: 'PUT', + path: opts.resource, + data: { + 'role-tag': opts.roleTags + } + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + + // --- Exports module.exports.createClient = function (options) { diff --git a/lib/do_rbac/do_info.js b/lib/do_rbac/do_info.js new file mode 100644 index 0000000..0f8a8b9 --- /dev/null +++ b/lib/do_rbac/do_info.js @@ -0,0 +1,272 @@ +/* + * 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 info ...` + */ + +/* BEGIN JSSTYLED */ +/* + +Sample output for development/discussion: + +``` +users ($numUsers): + bob ($fullname, **no ssh keys**): roles web* + carl (Carl Fogel): roles eng, operator*, cibot*, monbot* + ... +roles ($numRoles): + eng: policies write, read # include users here? + ops: policies delete, read, write + support: policies read +policies ($numPolicies): + delete ($desc): + can deletemachine + read (cloudapi read-only actions): + can listmachines, getmachine and listimages + write (cloudapi write (non-delete) actions): + can createmachine, updatemachine, stopmachine and startmachine +resources: # or call this 'resources'? role-tags? + # some dump of all resources (perhaps not default to *all*) and their + # role-tags + instance foo0 ($uuid): role-tags eng + image bar@1.2.3 ($uuid): role-tags ops +``` + +Ideas: +- red warning about users with no keys +- `triton rbac info -u bob` Show everything from bob's p.o.v. +- `triton rbac info -r readonly` Show everything from this role's p.o.v. + `... --instance foo0`, etc. +- `-t|--role-tags` to include the role tag info. Perhaps with arg for which? + E.g. do we traverse all machines, images, networks? That could too much... + Might need cloudapi support for returning those optionally. + ListImages?fields=*,role_tags # perhaps don't support '*' +*/ +/* END JSSTYLED */ + + +var assert = require('assert-plus'); +var format = require('util').format; +var tabula = require('tabula'); +var vasync = require('vasync'); + +var common = require('../common'); +var errors = require('../errors'); + +var ansiStylize = common.ansiStylize; + + + +/* + * Gather RBAC users, policies, roles and add those to the given `ctx` object. + */ +function gatherRbacBasicInfo(ctx, cb) { + assert.object(ctx.cloudapi, 'ctx.cloudapi'); + assert.func(cb, 'cb'); + + vasync.parallel({funcs: [ + function listUsers(next) { + ctx.cloudapi.listUsers(function (err, users) { + ctx.users = users; + next(err); + }); + }, + function listPolicies(next) { + ctx.cloudapi.listPolicies(function (err, policies) { + ctx.policies = policies; + next(err); + }); + }, + function listRoles(next) { + ctx.cloudapi.listRoles(function (err, roles) { + ctx.roles = roles; + next(err); + }); + } + ]}, cb); +} + + +function do_info(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 log = this.log; + + var context = { + tritonapi: this.top.tritonapi, + cloudapi: this.top.tritonapi.cloudapi + }; + vasync.pipeline({arg: context, funcs: [ + gatherRbacBasicInfo, + function gatherUserKeys(ctx, next) { + if (!opts.all) { + next(); + return; + } + // XXX Q! or concurrency forEachParallel + vasync.forEachParallel({ + inputs: ctx.users, + func: function oneUser(user, nextUser) { + ctx.cloudapi.listUserKeys({userId: user.id}, + function (err, userKeys) { + user.keys = userKeys; + nextUser(err); + }); + } + }, next); + }, + function fillInUserRoles(ctx, next) { + var i; + var userFromLogin = {}; + for (i = 0; i < ctx.users.length; i++) { + var user = ctx.users[i]; + user.default_roles = []; + user.roles = []; + userFromLogin[user.login] = user; + } + for (i = 0; i < ctx.roles.length; i++) { + var role = ctx.roles[i]; + role.default_members.forEach(function (login) { + userFromLogin[login].default_roles.push(role.name); + }); + role.members.forEach(function (login) { + userFromLogin[login].roles.push(role.name); + }); + } + next(); + }, + function printInfo(ctx, next) { + var i; + log.trace({ + users: ctx.users, + policies: ctx.policies, + roles: ctx.roles + }, 'rbac info data'); + + console.log('users (%d):', ctx.users.length); + tabula.sortArrayOfObjects(ctx.users, ['name']); + for (i = 0; i < ctx.users.length; i++) { + var user = ctx.users[i]; + + var userExtra = []; + if (user.firstName || user.lastName) { + userExtra.push(((user.firstName || '') + ' ' + + (user.lastName || '')).trim()); + } + if (user.keys && user.keys.length === 0) { + userExtra.push(ansiStylize('no ssh keys', 'red')); + } + if (userExtra.length > 0) { + userExtra = format(' (%s)', userExtra.join(', ')); + } else { + userExtra = ''; + } + + var roleInfo = []; + user.default_roles.sort(); + user.roles.sort(); + var roleSeen = {}; + user.default_roles.forEach(function (r) { + roleSeen[r] = true; + roleInfo.push(r); + }); + user.roles.forEach(function (r) { + if (!roleSeen[r]) { + roleInfo.push(r + '*'); // marker for non-default role + } + }); + if (roleInfo.length === 1) { + roleInfo = 'role ' + roleInfo.join(', '); + } else if (roleInfo.length > 0) { + roleInfo = 'roles ' + roleInfo.join(', '); + } else { + roleInfo = ansiStylize('no roles', 'red'); + } + console.log(' %s%s: %s', ansiStylize(user.login, 'bold'), + userExtra, roleInfo); + } + + console.log('roles (%d):', ctx.roles.length); + tabula.sortArrayOfObjects(ctx.roles, ['name']); + for (i = 0; i < ctx.roles.length; i++) { + var role = ctx.roles[i]; + + var policyInfo; + if (role.policies.length === 1) { + policyInfo = 'policy ' + role.policies.join(', '); + } else if (role.policies.length > 0) { + policyInfo = 'policies ' + role.policies.join(', '); + } else { + policyInfo = ansiStylize('no policies', 'red'); + } + console.log(' %s: %s', ansiStylize(role.name, 'bold'), + policyInfo); + } + + console.log('policies (%d):', ctx.policies.length); + tabula.sortArrayOfObjects(ctx.policies, ['name']); + for (i = 0; i < ctx.policies.length; i++) { + var policy = ctx.policies[i]; + var noRules = ''; + if (policy.rules.length === 0) { + noRules = ' ' + ansiStylize('no rules', 'red'); + } + if (policy.description) { + console.log(' %s (%s) rules:%s', + ansiStylize(policy.name, 'bold'), + policy.description, noRules); + } else { + console.log(' %s rules:%s', + ansiStylize(policy.name, 'bold'), noRules); + } + policy.rules.forEach(function (r) { + console.log(' %s', r); + }); + } + + next(); + } + ]}, function (err) { + cb(err); + }); +} + +do_info.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['all', 'a'], + type: 'bool', + help: 'Include all info for a more full report. This requires more ' + + 'work to gather all info.' + } +]; + +do_info.help = ( + /* BEGIN JSSTYLED */ + 'Print an account RBAC summary.\n' + + '\n' + + 'Usage:\n' + + ' {{name}} info []\n' + + '\n' + + '{{options}}' + /* END JSSTYLED */ +); + + + +module.exports = do_info; diff --git a/lib/do_rbac/do_role_tags.js b/lib/do_rbac/do_role_tags.js new file mode 100644 index 0000000..f309115 --- /dev/null +++ b/lib/do_rbac/do_role_tags.js @@ -0,0 +1,582 @@ +/* + * 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-tags ...` # hidden lower-level command + * `triton rbac instance-role-tags ...` + * `triton rbac image-role-tags ...` + * `triton rbac package-role-tags ...` + * `triton rbac network-role-tags ...` + * etc. + */ + +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'); + + +// ---- internal support stuff + +function _listRoleTags(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.resourceId, 'opts.resourceId'); + assert.optionalBool(opts.json, 'opts.json'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + cli.tritonapi.getInstanceRoleTags(opts.resourceId, + function (err, roleTags) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(roleTags)); + } else { + roleTags.forEach(function (r) { + console.log(r); + }); + } + cb(); + }); +} + +function _addRoleTags(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.resourceId, 'opts.resourceId'); + assert.arrayOfString(opts.roleTags, 'opts.roleTags'); + assert.func(cb, 'cb'); + var cli = opts.cli; + var log = cli.log; + + vasync.pipeline({arg: {}, funcs: [ + function getCurrRoleTags(ctx, next) { + cli.tritonapi.getInstanceRoleTags(opts.resourceId, + function (err, roleTags, inst) { + if (err) { + next(err); + return; + } + ctx.roleTags = roleTags; + ctx.inst = inst; + log.trace({inst: inst, roleTags: roleTags}, 'curr role tags'); + next(); + }); + }, + + function addRoleTags(ctx, next) { + var adding = []; + for (var i = 0; i < opts.roleTags.length; i++) { + var r = opts.roleTags[i]; + if (ctx.roleTags.indexOf(r) === -1) { + ctx.roleTags.push(r); + adding.push(r); + } + } + if (adding.length === 0) { + next(); + return; + } else { + console.log('Adding %d role tag%s (%s) to instance "%s"', + adding.length, adding.length === 1 ? '' : 's', + adding.join(', '), ctx.inst.name); + } + cli.tritonapi.cloudapi.setRoleTags({ + resource: _resourceUrlFromId( + cli.tritonapi.profile.account, ctx.inst.id), + roleTags: ctx.roleTags + }, next); + } + ]}, function (err) { + cb(err); + }); +} + + +// TODO: resource URL should be in tritonapi.js, +// E.g. perhaps `TritonApi.setInstanceRoleTags`? +function _resourceUrlFromId(account, id) { + return format('/%s/machines/%s', account, id); +} + + +function _reprFromRoleTags(roleTags) { + assert.arrayOfString(roleTags, 'roleTags'); + + if (roleTags.length === 0) { + return ''; + } + + // Make this somewhat canonical by sorting. + roleTags.sort(); + return roleTags.join('\n') + '\n'; +} + + +function _roleTagsFromRepr(repr) { + assert.string(repr, 'repr'); + + var roleTags = []; + var lines = repr.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; + } + roleTags.push(line); + }); + + roleTags.sort(); + return roleTags; +} + + +function _editRoleTags(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.resourceId, 'opts.resourceId'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + var account = cli.tritonapi.profile.account; + var id; + var roleTags; + 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.'); + 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 edited = _roleTagsFromRepr(afterText); + + if (_reprFromRoleTags(edited) === origText) { + // This repr is the closest to a canonical form we have. + console.log('No change'); + cb(); + return; + } + } catch (textErr) { + console.error('Error with your changes: %s', textErr); + offerRetry(afterText); + return; + } + + // Save changes. + cli.tritonapi.cloudapi.setRoleTags({ + resource: _resourceUrlFromId(account, id), + roleTags: edited + }, function (setErr) { + if (setErr) { + console.error('Error updating role tags with ' + + 'your changes: %s', setErr); + offerRetry(afterText); + return; + } + console.log('Edited role tags on instance "%s"', + opts.resourceId); + cb(); + }); + }); + } + + + cli.tritonapi.getInstanceRoleTags(opts.resourceId, + function (err, roleTags_, inst) { + if (err) { + cb(err); + return; + } + + id = inst.id; + roleTags = roleTags_; + filename = format('%s-inst-%s-roleTags.txt', + cli.tritonapi.profile.account, + opts.resourceId); + origText = _reprFromRoleTags(roleTags); + editAttempt(origText); + }); +} + + +function _setRoleTags(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.resourceId, 'opts.resourceId'); + assert.arrayOfString(opts.roleTags, 'opts.roleTags'); + assert.optionalBool(opts.yes, 'opts.yes'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + vasync.pipeline({arg: {}, funcs: [ + // TODO: consider shorter path if the instance UUID is given + // (but what if the instance has a UUID for an *alias*)? + function getResource(ctx, next) { + cli.tritonapi.getInstance(opts.resourceId, function (err, inst) { + if (err) { + next(err); + return; + } + ctx.inst = inst; + next(); + }); + }, + + function confirm(ctx, next) { + if (opts.yes) { + return next(); + } + var msg = format('Set role tags on instance "%s"? [y/n] ', + ctx.inst.name); + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + + function setThem(ctx, next) { + console.log('Setting role tags on instance "%s"', ctx.inst.name); + cli.tritonapi.cloudapi.setRoleTags({ + resource: _resourceUrlFromId( + cli.tritonapi.profile.account, ctx.inst.id), + roleTags: opts.roleTags + }, next); + } + ]}, function (err) { + cb(err); + }); +} + + + +function _deleteRoleTags(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.resourceId, 'opts.resourceId'); + assert.arrayOfString(opts.roleTags, 'opts.roleTags'); + assert.func(cb, 'cb'); + var cli = opts.cli; + var log = cli.log; + + vasync.pipeline({arg: {}, funcs: [ + function getCurrRoleTags(ctx, next) { + cli.tritonapi.getInstanceRoleTags(opts.resourceId, + function (err, roleTags, inst) { + if (err) { + next(err); + return; + } + ctx.roleTags = roleTags; + ctx.inst = inst; + log.trace({inst: inst, roleTags: roleTags}, 'curr role tags'); + next(); + }); + }, + + function determineToDelete(ctx, next) { + ctx.toDelete = []; + ctx.roleTagsToKeep = []; + for (var i = 0; i < ctx.roleTags.length; i++) { + var r = ctx.roleTags[i]; + if (opts.roleTags.indexOf(r) !== -1) { + ctx.toDelete.push(r); + } else { + ctx.roleTagsToKeep.push(r); + } + } + next(); + }, + + function confirm(ctx, next) { + if (ctx.toDelete.length === 0 || opts.yes) { + return next(); + } + var msg = format( + 'Delete %d role tag%s (%s) from instance "%s"? [y/n] ', + ctx.toDelete.length, ctx.toDelete.length === 1 ? '' : 's', + ctx.toDelete.join(', '), ctx.inst.name); + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + + function deleteRoleTags(ctx, next) { + if (ctx.toDelete.length === 0) { + next(); + return; + } + console.log('Deleting %d role tag%s (%s) from instance "%s"', + ctx.toDelete.length, ctx.toDelete.length === 1 ? '' : 's', + ctx.toDelete.join(', '), ctx.inst.name); + cli.tritonapi.cloudapi.setRoleTags({ + resource: _resourceUrlFromId( + cli.tritonapi.profile.account, ctx.inst.id), + roleTags: ctx.roleTagsToKeep + }, next); + } + ]}, function (err) { + cb(err); + }); +} + + +function _deleteAllRoleTags(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.resourceId, 'opts.resourceId'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + vasync.pipeline({arg: {}, funcs: [ + // TODO: consider shorter path if the instance UUID is given + // (but what if the instance has a UUID for an *alias*)? + function getResource(ctx, next) { + cli.tritonapi.getInstance(opts.resourceId, function (err, inst) { + if (err) { + next(err); + return; + } + ctx.inst = inst; + next(); + }); + }, + + function confirm(ctx, next) { + if (opts.yes) { + return next(); + } + var msg = format('Delete all role tags from instance "%s"? [y/n] ', + ctx.inst.name); + common.promptYesNo({msg: msg}, function (answer) { + if (answer !== 'y') { + console.error('Aborting'); + next(true); // early abort signal + } else { + next(); + } + }); + }, + + function deleteAllRoleTags(ctx, next) { + console.log('Deleting all role tags from instance "%s"', + ctx.inst.name); + cli.tritonapi.cloudapi.setRoleTags({ + resource: _resourceUrlFromId( + cli.tritonapi.profile.account, ctx.inst.id), + roleTags: [] + }, next); + } + ]}, function (err) { + cb(err); + }); +} + + +function _roleTagsFromArrayOfString(arr) { + assert.arrayOfString(arr, arr); + var allRoleTags = []; + for (var i = 0; i < arr.length; i++) { + var roleTags = arr[i] + /* JSSTYLED */ + .split(/\s*,\s*/) + .filter(function (r) { return r.trim(); }); + allRoleTags = allRoleTags.concat(roleTags); + } + return allRoleTags; +} + + +// ---- `triton rbac instance-role-tags` + +function do_instance_role_tags(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.set) { actions.push('set'); } + if (opts['delete']) { actions.push('delete'); } + if (opts.delete_all) { actions.push('deleteAll'); } + var action; + if (actions.length === 0) { + action = 'list'; + } 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 === 0) { + return cb(new errors.UsageError('INST argument is required')); + } else if (args.length > 1) { + return cb(new errors.UsageError('too many arguments')); + } + + switch (action) { + case 'list': + _listRoleTags({ + cli: this.top, + resourceId: args[0], + json: opts.json + }, cb); + break; + case 'add': + _addRoleTags({ + cli: this.top, + resourceId: args[0], + roleTags: _roleTagsFromArrayOfString(opts.add) + }, cb); + break; + case 'edit': + _editRoleTags({ + cli: this.top, + resourceId: args[0] + }, cb); + break; + case 'set': + _setRoleTags({ + cli: this.top, + resourceId: args[0], + roleTags: _roleTagsFromArrayOfString(opts.set), + yes: opts.yes + }, cb); + break; + case 'delete': + _deleteRoleTags({ + cli: this.top, + resourceId: args[0], + roleTags: _roleTagsFromArrayOfString(opts['delete']), + yes: opts.yes + }, cb); + break; + case 'deleteAll': + _deleteAllRoleTags({ + cli: this.top, + resourceId: args[0], + yes: opts.yes + }, cb); + break; + default: + return cb(new errors.InternalError('unknown action: ' + action)); + } +} + +do_instance_role_tags.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + }, + { + names: ['json', 'j'], + type: 'bool', + help: 'JSON stream output.' + }, + { + names: ['yes', 'y'], + type: 'bool', + help: 'Answer yes to confirmations, e.g. confirmation of deletion.' + }, + { + group: 'Action Options' + }, + { + names: ['add', 'a'], + type: 'arrayOfString', + helpArg: 'ROLE[,ROLE...]', + help: 'Add the given role tags. Can be specified multiple times.' + }, + { + names: ['set', 's'], + type: 'arrayOfString', + helpArg: 'ROLE[,ROLE...]', + help: 'Set role tags to the given value(s). Can be specified ' + + 'multiple times.' + }, + { + names: ['edit', 'e'], + type: 'bool', + help: 'Edit role tags in your $EDITOR.' + }, + { + names: ['delete', 'd'], + type: 'arrayOfString', + helpArg: 'ROLE[,ROLE...]', + help: 'Delete the given role tags. Can be specified multiple times.' + }, + { + names: ['delete-all', 'D'], + type: 'bool', + help: 'Delete all role tags from the given resource.' + } +]; +do_instance_role_tags.help = [ + /* BEGIN JSSTYLED */ + 'List and manage role tags for the given instance.', + '', + 'Usage:', + ' {{name}} instance-role-tags INST # list role tags', + ' {{name}} instance-role-tags -a ROLE[,ROLE...] INST # add', + ' {{name}} instance-role-tags -s ROLE[,ROLE...] INST # set/replace', + ' {{name}} instance-role-tags -e INST # edit in $EDITOR', + ' {{name}} instance-role-tags -d ROLE[,ROLE...] INST # delete', + ' {{name}} instance-role-tags -D INST # delete all', + '', + '{{options}}', + 'Where "ROLE" is a role tag name (see `triton rbac roles`) and INST is', + 'an instance "id", "name" or short id.' + /* END JSSTYLED */ +].join('\n'); + + + +module.exports = { + //do_role_tags: do_role_tags, + do_instance_role_tags: do_instance_role_tags +}; diff --git a/lib/do_rbac/index.js b/lib/do_rbac/index.js index c53d9e6..e7eb8bd 100644 --- a/lib/do_rbac/index.js +++ b/lib/do_rbac/index.js @@ -28,7 +28,22 @@ function RbacCLI(top) { /* END JSSTYLED */ helpOpts: { minHelpCol: 24 /* line up with option help */ - } + }, + helpSubcmds: [ + 'help', + 'info', + { group: 'RBAC Resources' }, + 'users', + 'user', + 'keys', + 'key', + 'policies', + 'policy', + 'roles', + 'role', + { group: 'Role Tags' }, + 'instance-role-tags' + ] }); } util.inherits(RbacCLI, Cmdln); @@ -40,17 +55,19 @@ RbacCLI.prototype.init = function init(opts, args, cb) { Cmdln.prototype.init.apply(this, arguments); }; +RbacCLI.prototype.do_info = require('./do_info'); RbacCLI.prototype.do_users = require('./do_users'); RbacCLI.prototype.do_user = require('./do_user'); - +RbacCLI.prototype.do_keys = require('./do_keys'); +RbacCLI.prototype.do_key = require('./do_key'); +RbacCLI.prototype.do_policies = require('./do_policies'); +RbacCLI.prototype.do_policy = require('./do_policy'); RbacCLI.prototype.do_roles = require('./do_roles'); RbacCLI.prototype.do_role = require('./do_role'); -RbacCLI.prototype.do_policies = require('./do_policies'); -RbacCLI.prototype.do_policy = require('./do_policy'); - -RbacCLI.prototype.do_keys = require('./do_keys'); -RbacCLI.prototype.do_key = require('./do_key'); +var doRoleTags = require('./do_role_tags'); +//RbacCLI.prototype.do_role_tags = doRoleTags.do_role_tags; +RbacCLI.prototype.do_instance_role_tags = doRoleTags.do_instance_role_tags; module.exports = RbacCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index cd423a5..6183df9 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -487,13 +487,16 @@ TritonApi.prototype.getNetwork = function getNetwork(name, cb) { * Get an instance by ID, exact name, or short ID, in that order. * * @param {String} name - * @param {Function} callback `function (err, inst)` + * @param {Function} callback `function (err, inst, res)` + * Where, on success, `res` is the response object from a `GetMachine` call + * if one was made. */ TritonApi.prototype.getInstance = function getInstance(name, cb) { var self = this; assert.string(name, 'name'); assert.func(cb, 'cb'); + var res; var shortId; var inst; @@ -511,7 +514,8 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) { return next(); } } - self.cloudapi.getMachine(uuid, function (err, inst_) { + self.cloudapi.getMachine(uuid, function (err, inst_, res_) { + res = res_; inst = inst_; if (err && err.restCode === 'ResourceNotFound') { // The CloudApi 404 error message sucks: "VM not found". @@ -579,7 +583,7 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) { if (err) { cb(err); } else if (inst) { - cb(null, inst); + cb(null, inst, res); } else { cb(new errors.ResourceNotFoundError(format( 'no instance with name or short id "%s" was found', name))); @@ -588,6 +592,62 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) { }; +/** + * Get instance role tags. + * + * @param {String} name The instance id, name, or short id. + * @param {Function} callback `function (err, roleTags, inst)` + */ +TritonApi.prototype.getInstanceRoleTags = +function getInstanceRoleTags(name, cb) { + var self = this; + assert.string(name, 'name'); + assert.func(cb, 'cb'); + + var roleTags; + var inst; + + vasync.pipeline({arg: {}, funcs: [ + function getInst(ctx, next) { + self.getInstance(name, function (err, inst_, res) { + if (err) { + next(err); + return; + } + inst = inst_; + ctx.res = res; + next(); + }); + }, + function getMachineIfNecessary(ctx, next) { + // Sometimes `getInstance` returns a CloudAPI `GetMachine` res on + // which there is a 'role-tag' header that we want. Sometimes not. + if (ctx.res) { + next(); + return; + } + + self.cloudapi.getMachine(inst.id, function (err, inst_, res) { + if (err) { + next(err); + return; + } + ctx.res = res; + next(); + }); + }, + function getRolesFromRes(ctx, next) { + roleTags = (ctx.res.headers['role-tag'] || '') + /* JSSTYLED */ + .split(/\s*,\s*/) + .filter(function (r) { return r.trim(); }); + next(); + } + ]}, function (err) { + cb(err, roleTags, inst); + }); +}; + /** * Get an RBAC user by ID, login, or short ID, in that order. @@ -715,7 +775,8 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { next(); return; } - self.cloudapi.listUserKeys({id: ctx.user.id}, function (err, keys) { + self.cloudapi.listUserKeys({userId: ctx.user.id}, + function (err, keys) { if (err) { next(err); return;