From c7daecc6f39b9959d21075eae7c3daa38cb0004d Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 5 Nov 2015 12:30:06 -0800 Subject: [PATCH] joyent/node-triton#54 first pass at 'triton rbac policy' and 'triton rbac policies' --- CHANGES.md | 2 + lib/cloudapi2.js | 136 +++++++++- lib/common.js | 12 +- lib/do_rbac/do_policies.js | 105 ++++++++ lib/do_rbac/do_policy.js | 539 +++++++++++++++++++++++++++++++++++++ lib/do_rbac/do_role.js | 35 +-- lib/do_rbac/do_user.js | 12 +- lib/do_rbac/index.js | 3 + lib/tritonapi.js | 145 +++++++++- 9 files changed, 943 insertions(+), 46 deletions(-) create mode 100644 lib/do_rbac/do_policies.js create mode 100644 lib/do_rbac/do_policy.js diff --git a/CHANGES.md b/CHANGES.md index bb0999a..c057c78 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ - `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. + - `triton rbac policies` to list all policies. + - `triton rbac policy ...` to show, create, edit and delete policies. ## 2.1.4 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index ae4cdef..5c4a8e3 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -460,7 +460,7 @@ CloudApi.prototype.getMachine = function getMachine(id, cb) { * delete a machine by id. * * @param {String} uuid (required) The machine id. - * @param {Function} callback of the form `function (err, machine, response)` + * @param {Function} callback of the form `function (err, res)` */ CloudApi.prototype.deleteMachine = function deleteMachine(uuid, callback) { var self = this; @@ -471,8 +471,8 @@ CloudApi.prototype.deleteMachine = function deleteMachine(uuid, callback) { path: format('/%s/machines/%s', self.account, uuid), method: 'DELETE' }; - this._request(opts, function (err, req, res, body) { - callback(err, body, res); + this._request(opts, function (err, req, res) { + callback(err, res); }); }; @@ -809,9 +809,9 @@ CloudApi.prototype.updateUser = function updateUser(opts, cb) { /** * * - * @param {Object} opts (object) user object containing: + * @param {Object} opts (object) * - {String} id (required) for your user. - * @param {Function} cb of the form `function (err, user, res)` + * @param {Function} cb of the form `function (err, res)` */ CloudApi.prototype.deleteUser = function deleteUser(opts, cb) { assert.object(opts, 'opts'); @@ -821,8 +821,8 @@ CloudApi.prototype.deleteUser = function deleteUser(opts, cb) { this._request({ method: 'DELETE', path: format('/%s/users/%s', this.account, opts.id) - }, function (err, req, res, body) { - cb(err, body, res); + }, function (err, req, res) { + cb(err, res); }); }; @@ -850,7 +850,7 @@ CloudApi.prototype.listRoles = function listRoles(opts, cb) { * * @param {Object} opts * - id {UUID|String} The role ID or name. - * @param {Function} callback of the form `function (err, user, res)` + * @param {Function} callback of the form `function (err, role, res)` */ CloudApi.prototype.getRole = function getRole(opts, cb) { assert.object(opts, 'opts'); @@ -930,9 +930,9 @@ CloudApi.prototype.updateRole = function updateRole(opts, cb) { /** * * - * @param {Object} opts (object) user object containing: + * @param {Object} opts (object) * - {String} id (required) of the role to delete. - * @param {Function} cb of the form `function (err, user, res)` + * @param {Function} cb of the form `function (err, res)` */ CloudApi.prototype.deleteRole = function deleteRole(opts, cb) { assert.object(opts, 'opts'); @@ -942,11 +942,127 @@ CloudApi.prototype.deleteRole = function deleteRole(opts, cb) { this._request({ method: 'DELETE', path: format('/%s/roles/%s', this.account, opts.id) + }, function (err, req, res) { + cb(err, res); + }); +}; + + +/** + * + * + * @param opts {Object} Options (optional) + * @param cb {Function} Callback of the form `function (err, policies, res)` + */ +CloudApi.prototype.listPolicies = function listPolicies(opts, cb) { + if (cb === undefined) { + cb = opts; + opts = {}; + } + assert.func(cb, 'cb'); + assert.object(opts, 'opts'); + + var endpoint = format('/%s/policies', this.account); + this._passThrough(endpoint, opts, cb); +}; + +/** + * + * + * @param {Object} opts + * - id {UUID|String} The policy ID or name. + * @param {Function} callback of the form `function (err, policy, res)` + */ +CloudApi.prototype.getPolicy = function getPolicy(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/policies/%s', this.account, opts.id); + this._passThrough(endpoint, {}, cb); +}; + +/** + * + * + * @param {Object} opts (object) policy object containing: + * - {String} name (required) for the policy. + * - {Array} description (optional) for the policy. + * - {Array} rules (optional) for the policy. + * @param {Function} cb of the form `function (err, policy, res)` + */ +CloudApi.prototype.createPolicy = function createPolicy(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, + description: opts.description, + rules: opts.rules + }; + + this._request({ + method: 'POST', + path: format('/%s/policies', this.account), + data: data }, function (err, req, res, body) { cb(err, body, res); }); }; +/** + * + * + * @param {Object} opts (object) policy object containing: + * - {UUID|String} id (required) The policy ID or name. + * - {String} name (optional) + * - {String} description (optional) + * - {Array} rules (optional) + * @param {Function} cb of the form `function (err, policy, res)` + */ +CloudApi.prototype.updatePolicy = function updatePolicy(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, + description: opts.description, + rules: opts.rules + }; + + this._request({ + method: 'POST', + path: format('/%s/policies/%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 policy to delete. + * @param {Function} cb of the form `function (err, res)` + */ +CloudApi.prototype.deletePolicy = function deletePolicy(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/policies/%s', this.account, opts.id) + }, function (err, req, res) { + cb(err, res); + }); +}; // --- Exports diff --git a/lib/common.js b/lib/common.js index 6c30010..0d51cd0 100644 --- a/lib/common.js +++ b/lib/common.js @@ -686,6 +686,15 @@ function ansiStylize(str, color) { } +function indent(s, indentation) { + if (!indentation) { + indentation = ' '; + } + var lines = s.split(/\r?\n/g); + return indentation + lines.join('\n' + indentation); +} + + //---- exports @@ -709,6 +718,7 @@ module.exports = { promptEnter: promptEnter, promptField: promptField, editInEditor: editInEditor, - ansiStylize: ansiStylize + ansiStylize: ansiStylize, + indent: indent }; // vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/do_rbac/do_policies.js b/lib/do_rbac/do_policies.js new file mode 100644 index 0000000..0cb74a4 --- /dev/null +++ b/lib/do_rbac/do_policies.js @@ -0,0 +1,105 @@ +/* + * 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 policies ...` + */ + +var tabula = require('tabula'); + +var common = require('../common'); +var errors = require('../errors'); + + + +// columns default without -o +var columnsDefault = 'shortid,name,description,nrules'; + +// columns default with -l +var columnsDefaultLong = 'id,name,rules'; + +// sort default with -s +var sortDefault = 'name'; + + +function do_policies(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.listPolicies(function (err, policies) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + common.jsonStream(policies); + } else { + var i, j; + // Add some convenience fields + for (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('; '); + } + + tabula(policies, { + skipHeader: opts.H, + columns: columns, + sort: sort + }); + } + cb(); + }); +} + +do_policies.options = [ + { + names: ['help', 'h'], + type: 'bool', + help: 'Show this help.' + } +].concat(common.getCliTableOptions({ + includeLong: true, + sortDefault: sortDefault +})); + +do_policies.help = ( + /* BEGIN JSSTYLED */ + 'List RBAC policies.\n' + + '\n' + + 'Usage:\n' + + ' {{name}} policies []\n' + + '\n' + + '{{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 */ +); + + + +module.exports = do_policies; diff --git a/lib/do_rbac/do_policy.js b/lib/do_rbac/do_policy.js new file mode 100644 index 0000000..42d96ea --- /dev/null +++ b/lib/do_rbac/do_policy.js @@ -0,0 +1,539 @@ +/* + * 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 policy ...` + */ + +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: 'description'}, + // Want 'rules' last for multiple yamlish repr, see below. + {key: 'rules', array: true} +]; + +var CREATE_ROLE_FIELDS = [ + {key: 'name', required: true}, + {key: 'description'}, + {key: 'rules', array: true} +]; + +function _showPolicy(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + cli.tritonapi.getPolicy({ + id: opts.id + }, function onPolicy(err, policy) { + if (err) { + cb(err); + return; + } + + if (opts.json) { + console.log(JSON.stringify(policy)); + } else { + console.log('name: %s', policy.name); + delete policy.name; + var rules = policy.rules; + delete policy.rules; + Object.keys(policy).forEach(function (key) { + console.log('%s: %s', key, policy[key]); + }); + // Do rules last because it is the sole multiline field. The + // rules can tend to be long, so we want to use multiline output. + console.log('rules:'); + if (rules && rules.length) { + console.log(' ' + rules.join('\n ')); + } + } + cb(); + }); +} + +function _yamlishFromPolicy(policy) { + assert.object(policy, 'policy'); + + var lines = []; + UPDATABLE_ROLE_FIELDS.forEach(function (field) { + var key = field.key; + var val = policy[key]; + if (key === 'rules') { + lines.push('rules:'); + if (val && val.length) { + lines.push(' ' + val.join('\n ')); + } + } else { + lines.push(format('%s: %s', key, val)); + } + }); + return lines.join('\n') + '\n'; +} + +function _stripYamlishLine(line) { + var commentIdx = line.indexOf('#'); + if (commentIdx !== -1) { + line = line.slice(0, commentIdx); + } + return line.trim(); +} + +function _policyFromYamlish(yamlish) { + assert.string(yamlish, 'yamlish'); + + var policy = {}; + var lines = yamlish.split(/\n/g); + for (var i = 0; i < lines.length; i++) { + var line = _stripYamlishLine(lines[i]); + if (!line) { + continue; + } + var parts = strsplit(line, ':', 2); + var key = parts[0].trim(); + var value = parts[1].trim(); + if (key === 'rules') { + rules = []; + if (value) { + rules.push(value); + } + // Remaining lines are rules. + for (var j = i+1; j < lines.length; j++) { + var line = _stripYamlishLine(lines[j]); + if (!line) { + continue; + } + rules.push(line); + } + policy['rules'] = rules; + break; + } else { + policy[key] = value; + } + } + + return policy; +} + + +function _editPolicy(opts, cb) { + assert.object(opts.cli, 'opts.cli'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + var cli = opts.cli; + + var policy; + 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 policy.'); + 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 editedPolicy = _policyFromYamlish(afterText); + editedPolicy.id = policy.id; + + if (_yamlishFromPolicy(editedPolicy) === origText) { + // This YAMLish is the closest to a canonical form we have. + console.log('No change to policy'); + cb(); + return; + } + } catch (textErr) { + console.error('Error with your changes: %s', textErr); + offerRetry(afterText); + return; + } + + // Save changes. + cli.tritonapi.cloudapi.updatePolicy(editedPolicy, + function (uErr, updated) { + if (uErr) { + var prefix = 'Error updating policy with your changes:' + var errmsg = uErr.toString(); + if (errmsg.indexOf('\n') !== -1) { + console.error(prefix + '\n' + common.indent(errmsg)); + } else { + console.error(prefix + ' ' + errmsg); + } + offerRetry(afterText); + return; + } + console.log('Updated policy "%s" (%s)', + updated.name, updated.id); + cb(); + }); + }); + } + + + cli.tritonapi.getPolicy({ + id: opts.id + }, function onPolicy(err, policy_) { + if (err) { + return cb(err); + } + + policy = policy_; + filename = format('%s-policy-%s.txt', cli.tritonapi.profile.account, + policy.name); + origText = _yamlishFromPolicy(policy); + editAttempt(origText); + }); +} + + +function _deletePolicies(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 policy "' + opts.ids[0] + '"? [y/n] '; + } else { + msg = format('Delete %d policies (%s)? [y/n] ', + opts.ids.length, opts.ids.join(', ')); + } + 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.deletePolicy({id: id}, function (err) { + if (err) { + nextId(err); + return; + } + console.log('Deleted policy "%s"', id); + nextId(); + }); + } + }, next); + } + ]}, function (err) { + if (err === true) { + err = null; + } + cb(err); + }); +} + + +function _addPolicy(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 policy JSON on stdin'); + return next(new errors.TritonError( + format('invalid policy 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 policy 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 policy: stdin is not a TTY')); + } else if (!process.stdout.isTTY) { + return next(new errors.UsageError('cannot interactively ' + + 'create a policy: stdout is not a TTY')); + } + + // TODO: better validation of name, rules + // 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) { + if (field.key === 'rules') { + var rules = []; + var rulePrompt = { + key: 'rule', + desc: 'Enter one rule per line. Enter an empty ' + + 'rule to finish rules. See ' + + // JSSTYLED + ' ' + + 'for rule syntax and examples.' + }; + var promptAnotherRule = function () { + common.promptField(rulePrompt, function (err, val) { + delete rulePrompt.desc; // only want first time + if (err) { + nextField(err); + } else if (!val) { + // Done rules. + data.rules = rules; + nextField(); + } else { + rules.push(val); + promptAnotherRule(); + } + }); + }; + promptAnotherRule(); + } else { + common.promptField(field, function (err, value) { + if (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 policy data: ' + issues.join('; '))); + } else { + next(); + } + }, + function createIt(_, next) { + cli.tritonapi.cloudapi.createPolicy(data, function (err, policy) { + if (err) { + next(err); + return; + } + console.log('Created policy "%s"', policy.name); + next(); + }); + } + ]}, cb); +} + + +function do_policy(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 === 0 && ['show', 'edit'].indexOf(action) !== -1) { + return cb(new errors.UsageError('POLICY argument is required')); + } else if (action !== 'delete' && args.length > 1) { + return cb(new errors.UsageError('too many arguments')); + } + + switch (action) { + case 'show': + _showPolicy({ + cli: this.top, + id: args[0], + json: opts.json + }, cb); + break; + case 'edit': + _editPolicy({ + cli: this.top, + id: args[0] + }, cb); + break; + case 'delete': + _deletePolicies({ + cli: this.top, + ids: args, + yes: opts.yes + }, cb); + break; + case 'add': + _addPolicy({cli: this.top, file: args[0]}, cb); + break; + default: + return cb(new errors.InternalError('unknown action: ' + action)); + } +} + +do_policy.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: ['edit', 'e'], + type: 'bool', + help: 'Edit the named policy in your $EDITOR.' + }, + { + names: ['add', 'a'], + type: 'bool', + help: 'Add a new policy.' + }, + { + names: ['delete', 'd'], + type: 'bool', + help: 'Delete the named policy.' + } +]; +do_policy.help = [ + /* BEGIN JSSTYLED */ + 'Show, add, edit and delete RBAC policies.', + '', + 'Usage:', + ' {{name}} policy POLICY # show policy POLICY', + ' {{name}} policy -e|--edit POLICY # edit policy POLICY in $EDITOR', + ' {{name}} policy -d|--delete [POLICY...] # delete policy POLICY', + '', + ' {{name}} policy -a|--add [FILE]', + ' # Add a new policy. FILE must be a file path to a JSON file', + ' # with the policy data or "-" to pass the policy in on stdin.', + ' # Or exclude FILE to interactively add.', + '', + '{{options}}', + 'Where "POLICY" is a full policy "id", the policy "login" name or a "shortid", i.e.', + 'an id prefix.', + '', + 'Fields for creating a policy:', + CREATE_ROLE_FIELDS.map(function (field) { + return ' ' + field.key + (field.required ? ' (required)' : ''); + }).join('\n') + /* END JSSTYLED */ +].join('\n'); + +module.exports = do_policy; diff --git a/lib/do_rbac/do_role.js b/lib/do_rbac/do_role.js index 89e56af..9c23d42 100644 --- a/lib/do_rbac/do_role.js +++ b/lib/do_rbac/do_role.js @@ -49,13 +49,11 @@ function _arrayFromCSV(csv) { 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 + id: opts.id }, function onRole(err, role) { if (err) { cb(err); @@ -222,8 +220,8 @@ function _deleteRoles(opts, cb) { if (opts.ids.length === 1) { msg = 'Delete role "' + opts.ids[0] + '"? [y/n] '; } else { - msg = 'Delete %d roles (' + opts.ids.join(', ') + ')? [y/n] '; - + msg = format('Delete %d roles (%s)? [y/n] ', + opts.ids.length, opts.ids.join(', ')); } common.promptYesNo({msg: msg}, function (answer) { if (answer !== 'y') { @@ -405,12 +403,10 @@ function do_role(subcmd, opts, args, cb) { } // 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) - { + if (args.length === 0 && ['show', 'edit'].indexOf(action) !== -1) { return cb(new errors.UsageError('ROLE argument is required')); + } else if (action !== 'delete' && args.length > 1) { + return cb(new errors.UsageError('too many arguments')); } switch (action) { @@ -453,19 +449,6 @@ do_role.options = [ 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', @@ -477,17 +460,17 @@ do_role.options = [ { names: ['edit', 'e'], type: 'bool', - help: 'Edit the named user in your $EDITOR.' + help: 'Edit the named role in your $EDITOR.' }, { names: ['add', 'a'], type: 'bool', - help: 'Add a new user.' + help: 'Add a new role.' }, { names: ['delete', 'd'], type: 'bool', - help: 'Delete the named user.' + help: 'Delete the named role.' } ]; do_role.help = [ diff --git a/lib/do_rbac/do_user.js b/lib/do_rbac/do_user.js index 1a86689..a7987e3 100644 --- a/lib/do_rbac/do_user.js +++ b/lib/do_rbac/do_user.js @@ -211,8 +211,8 @@ function _deleteUsers(opts, cb) { if (opts.ids.length === 1) { msg = 'Delete user "' + opts.ids[0] + '"? [y/n] '; } else { - msg = 'Delete %d users (' + opts.ids.join(', ') + ')? [y/n] '; - + msg = format('Delete %d users (%s)? [y/n] ', + opts.ids.length, opts.ids.join(', ')); } common.promptYesNo({msg: msg}, function (answer) { if (answer !== 'y') { @@ -385,12 +385,10 @@ function do_user(subcmd, opts, args, cb) { } // 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) - { + if (args.length === 0 && ['show', 'edit'].indexOf(action) !== -1) { return cb(new errors.UsageError('USER argument is required')); + } else if (action !== 'delete' && args.length > 1) { + return cb(new errors.UsageError('too many arguments')); } switch (action) { diff --git a/lib/do_rbac/index.js b/lib/do_rbac/index.js index 2e3a511..784af4b 100644 --- a/lib/do_rbac/index.js +++ b/lib/do_rbac/index.js @@ -47,4 +47,7 @@ RbacCLI.prototype.do_user = require('./do_user'); 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'); + module.exports = RbacCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index b677898..2846029 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -811,7 +811,7 @@ TritonApi.prototype.getRole = function getRole(opts, cb) { * * @param {Object} opts * - id {UUID|String} The role id (a UUID), name or short id. - * @param {Function} callback of the form `function (err, user)` + * @param {Function} callback of the form `function (err)` */ TritonApi.prototype.deleteRole = function deleteRole(opts, cb) { var self = this; @@ -832,8 +832,12 @@ TritonApi.prototype.deleteRole = function deleteRole(opts, cb) { } self.getRole({id: opts.id}, function (err, role) { + if (err) { + next(err); + return; + } ctx.id = role.id; - next(err); + next(); }); }, @@ -846,6 +850,143 @@ TritonApi.prototype.deleteRole = function deleteRole(opts, cb) { }; + +/** + * Get an RBAC policy by ID, name, or short ID, in that order. + * + * @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. + * @param {Function} callback of the form `function (err)` + */ +TritonApi.prototype.deletePolicy = function deletePolicy(opts, cb) { + var self = this; + assert.object(opts, 'opts'); + assert.string(opts.id, 'opts.id'); + assert.func(cb, 'cb'); + + /* + * CloudAPI DeletePolicy only accepts a policy 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.getPolicy({id: opts.id}, function (err, policy) { + if (err) { + next(err); + return; + } + ctx.id = policy.id; + next(); + }); + }, + + function deleteIt(ctx, next) { + self.cloudapi.deletePolicy({id: ctx.id}, next); + } + ]}, function (err) { + cb(err); + }); +}; + + //---- exports module.exports.createClient = function (options) {