diff --git a/CHANGES.md b/CHANGES.md index c057c78..b151a62 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,8 @@ - `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. + - `triton rbac keys` to list all RBAC user SSH keys. + - `triton rbac key ...` to show, create, edit and delete user keys. ## 2.1.4 diff --git a/lib/cloudapi2.js b/lib/cloudapi2.js index 5c4a8e3..92d0101 100644 --- a/lib/cloudapi2.js +++ b/lib/cloudapi2.js @@ -294,7 +294,7 @@ CloudApi.prototype._passThrough = function _passThrough(endpoint, opts, cb) { /** * Get network information * - * @param {Function} callback of the form `function (err, networks, response)` + * @param {Function} callback of the form `function (err, networks, res)` */ CloudApi.prototype.listNetworks = function listNetworks(opts, cb) { var endpoint = format('/%s/networks', this.account); @@ -324,7 +324,7 @@ CloudApi.prototype.getNetwork = function getNetwork(id, cb) { /** * Get services information * - * @param {Function} callback of the form `function (err, services, response)` + * @param {Function} callback of the form `function (err, services, res)` */ CloudApi.prototype.listServices = function listServices(opts, cb) { var endpoint = format('/%s/services', this.account); @@ -334,8 +334,7 @@ CloudApi.prototype.listServices = function listServices(opts, cb) { /** * Get datacenters information * - * @param {Function} callback of the form - * `function (err, datacenters, response)` + * @param {Function} callback of the form `function (err, datacenters, res)` */ CloudApi.prototype.listDatacenters = function listDatacenters(opts, cb) { var endpoint = format('/%s/datacenters', this.account); @@ -348,7 +347,7 @@ CloudApi.prototype.listDatacenters = function listDatacenters(opts, cb) { /** * Get account information * - * @param {Function} callback of the form `function (err, account, response)` + * @param {Function} callback of the form `function (err, account, res)` */ CloudApi.prototype.getAccount = function getAccount(opts, cb) { var endpoint = format('/%s', this.account); @@ -356,9 +355,9 @@ CloudApi.prototype.getAccount = function getAccount(opts, cb) { }; /** - * Get public key information + * List account's SSH keys. * - * @param {Function} callback of the form `function (err, keys, response)` + * @param {Function} callback of the form `function (err, keys, res)` */ CloudApi.prototype.listKeys = function listKeys(opts, cb) { var endpoint = format('/%s/keys', this.account); @@ -444,7 +443,7 @@ CloudApi.prototype.getPackage = function getPackage(opts, cb) { * XXX cloudapi docs don't doc the credentials=true option * * @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, machine, res)` */ CloudApi.prototype.getMachine = function getMachine(id, cb) { assert.uuid(id, 'id'); @@ -480,7 +479,7 @@ CloudApi.prototype.deleteMachine = function deleteMachine(uuid, callback) { * start 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, machine, res)` */ CloudApi.prototype.startMachine = function startMachine(uuid, callback) { return this._doMachine('start', uuid, callback); @@ -490,7 +489,7 @@ CloudApi.prototype.startMachine = function startMachine(uuid, callback) { * stop 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, machine, res)` */ CloudApi.prototype.stopMachine = function stopMachine(uuid, callback) { return this._doMachine('stop', uuid, callback); @@ -500,7 +499,7 @@ CloudApi.prototype.stopMachine = function stopMachine(uuid, callback) { * reboot 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, machine, res)` */ CloudApi.prototype.rebootMachine = function rebootMachine(uuid, callback) { return this._doMachine('reboot', uuid, callback); @@ -658,7 +657,7 @@ CloudApi.prototype.createMachine = function createMachine(options, callback) { * XXX IMO this endpoint should be called ListMachineAudit in cloudapi. * * @param {String} id (required) The machine id. - * @param {Function} callback of the form `function (err, audit, response)` + * @param {Function} callback of the form `function (err, audit, res)` */ CloudApi.prototype.machineAudit = function machineAudit(id, cb) { assert.uuid(id, 'id'); @@ -827,6 +826,112 @@ CloudApi.prototype.deleteUser = function deleteUser(opts, cb) { }; + +/** + * List RBAC user's SSH keys. + * + * @param {Object} opts (object) + * - {String} userId (required) The user id or login. + * @param {Function} callback of the form `function (err, userKeys, res)` + */ +CloudApi.prototype.listUserKeys = function listUserKeys(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.userId, 'opts.userId'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/users/%s/keys', this.account, opts.userId); + this._passThrough(endpoint, opts, cb); +}; + + +/** + * Get a RBAC user's SSH key. + * + * @param {Object} opts (object) + * - {String} userId (required) The user id or login. + * - {String} fingerprint (required*) The SSH key fingerprint. One of + * 'fingerprint' or 'name' is required. + * - {String} name (required*) The SSH key name. One of 'fingerprint' + * or 'name' is required. + * @param {Function} callback of the form `function (err, userKey, res)` + */ +CloudApi.prototype.getUserKey = function getUserKey(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.userId, 'opts.userId'); + assert.optionalString(opts.fingerprint, 'opts.fingerprint'); + assert.optionalString(opts.name, 'opts.name'); + assert.ok(opts.fingerprint || opts.name, + 'one of "fingerprint" or "name" is require'); + assert.func(cb, 'cb'); + + var endpoint = format('/%s/users/%s/keys/%s', this.account, opts.userId, + encodeURIComponent(opts.fingerprint || opts.name)); + this._passThrough(endpoint, {}, cb); +}; + + +/** + * Create/upload a new RBAC user SSH public key. + * + * @param {Object} opts (object) + * - {String} userId (required) The user id or login. + * - {String} key (required) The SSH public key content. + * - {String} name (optional) A name for the key. If not given, the + * key fingerprint will be used. + * @param {Function} callback of the form `function (err, userKey, res)` + */ +CloudApi.prototype.createUserKey = function createUserKey(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.userId, 'opts.userId'); + assert.string(opts.key, 'opts.key'); + assert.optionalString(opts.name, 'opts.name'); + assert.func(cb, 'cb'); + + var data = { + name: opts.name, + key: opts.key + }; + + this._request({ + method: 'POST', + path: format('/%s/users/%s/keys', this.account, opts.userId), + data: data + }, function (err, req, res, body) { + cb(err, body, res); + }); +}; + + +/** + * Delete a RBAC user's SSH key. + * + * @param {Object} opts (object) + * - {String} userId (required) The user id or login. + * - {String} fingerprint (required*) The SSH key fingerprint. One of + * 'fingerprint' or 'name' is required. + * - {String} name (required*) The SSH key name. One of 'fingerprint' + * or 'name' is required. + * @param {Function} callback of the form `function (err, res)` + */ +CloudApi.prototype.deleteUserKey = function deleteUserKey(opts, cb) { + assert.object(opts, 'opts'); + assert.string(opts.userId, 'opts.userId'); + assert.optionalString(opts.fingerprint, 'opts.fingerprint'); + assert.optionalString(opts.name, 'opts.name'); + assert.ok(opts.fingerprint || opts.name, + 'one of "fingerprint" or "name" is require'); + assert.func(cb, 'cb'); + + this._request({ + method: 'DELETE', + path: format('/%s/users/%s/keys/%s', this.account, opts.userId, + encodeURIComponent(opts.fingerprint || opts.name)) + }, function (err, req, res) { + cb(err, res); + }); +}; + + /** * * diff --git a/lib/common.js b/lib/common.js index 0d51cd0..0369b6f 100644 --- a/lib/common.js +++ b/lib/common.js @@ -695,6 +695,17 @@ function indent(s, indentation) { } +// http://perldoc.perl.org/functions/chomp.html +function chomp(s) { + if (s.length) { + while (s.slice(-1) === '\n') { + s = s.slice(0, -1); + } + } + return s; +} + + //---- exports @@ -719,6 +730,7 @@ module.exports = { promptField: promptField, editInEditor: editInEditor, ansiStylize: ansiStylize, - indent: indent + indent: indent, + chomp: chomp }; // vim: set softtabstop=4 shiftwidth=4: diff --git a/lib/do_keys.js b/lib/do_keys.js index b2efe7a..877e682 100644 --- a/lib/do_keys.js +++ b/lib/do_keys.js @@ -11,19 +11,21 @@ */ var common = require('./common'); +var errors = require('./errors'); -function do_keys(subcmd, opts, args, callback) { + +function do_keys(subcmd, opts, args, cb) { if (opts.help) { - this.do_help('help', {}, [subcmd], callback); + this.do_help('help', {}, [subcmd], cb); return; } else if (args.length !== 0) { - callback(new Error('invalid args: ' + args)); + cb(new errors.UsageError('invalid args: ' + args)); return; } this.tritonapi.cloudapi.listKeys(function (err, keys) { if (err) { - callback(err); + cb(err); return; } @@ -31,13 +33,10 @@ function do_keys(subcmd, opts, args, callback) { common.jsonStream(keys); } else { keys.forEach(function (key) { - process.stdout.write(key.key); - if (key.key && key.key.slice(-1) !== '\n') { - process.stdout.write('\n'); - } + console.log(common.chomp(key.key)); }); } - callback(); + cb(); }); } diff --git a/lib/do_rbac/do_policies.js b/lib/do_rbac/do_policies.js index 0cb74a4..f6e305e 100644 --- a/lib/do_rbac/do_policies.js +++ b/lib/do_rbac/do_policies.js @@ -55,9 +55,8 @@ function do_policies(subcmd, opts, args, cb) { if (opts.json) { common.jsonStream(policies); } else { - var i, j; // Add some convenience fields - for (i = 0; i < policies.length; i++) { + for (var i = 0; i < policies.length; i++) { var role = policies[i]; role.shortid = role.id.split('-', 1)[0]; role.nrules = role.rules.length; diff --git a/lib/do_rbac/do_policy.js b/lib/do_rbac/do_policy.js index 42d96ea..d29849a 100644 --- a/lib/do_rbac/do_policy.js +++ b/lib/do_rbac/do_policy.js @@ -98,10 +98,11 @@ function _stripYamlishLine(line) { function _policyFromYamlish(yamlish) { assert.string(yamlish, 'yamlish'); + var line; var policy = {}; var lines = yamlish.split(/\n/g); for (var i = 0; i < lines.length; i++) { - var line = _stripYamlishLine(lines[i]); + line = _stripYamlishLine(lines[i]); if (!line) { continue; } @@ -109,13 +110,13 @@ function _policyFromYamlish(yamlish) { var key = parts[0].trim(); var value = parts[1].trim(); if (key === 'rules') { - rules = []; + var rules = []; if (value) { rules.push(value); } // Remaining lines are rules. for (var j = i+1; j < lines.length; j++) { - var line = _stripYamlishLine(lines[j]); + line = _stripYamlishLine(lines[j]); if (!line) { continue; } @@ -185,7 +186,7 @@ function _editPolicy(opts, cb) { cli.tritonapi.cloudapi.updatePolicy(editedPolicy, function (uErr, updated) { if (uErr) { - var prefix = 'Error updating policy with your changes:' + var prefix = 'Error updating policy with your changes:'; var errmsg = uErr.toString(); if (errmsg.indexOf('\n') !== -1) { console.error(prefix + '\n' + common.indent(errmsg)); @@ -526,7 +527,7 @@ do_policy.help = [ ' # Or exclude FILE to interactively add.', '', '{{options}}', - 'Where "POLICY" is a full policy "id", the policy "login" name or a "shortid", i.e.', + 'Where "POLICY" is a full policy "id", the policy "name" or a "shortid", i.e.', 'an id prefix.', '', 'Fields for creating a policy:', diff --git a/lib/do_rbac/do_role.js b/lib/do_rbac/do_role.js index 9c23d42..288cd0f 100644 --- a/lib/do_rbac/do_role.js +++ b/lib/do_rbac/do_role.js @@ -488,7 +488,7 @@ do_role.help = [ ' # Or exclude FILE to interactively add.', '', '{{options}}', - 'Where "ROLE" is a full role "id", the role "login" name or a "shortid", i.e.', + 'Where "ROLE" is a full role "id", the role "name" or a "shortid", i.e.', 'an id prefix.', '', 'Fields for creating a role:', diff --git a/lib/do_rbac/do_user.js b/lib/do_rbac/do_user.js index a7987e3..4b55df9 100644 --- a/lib/do_rbac/do_user.js +++ b/lib/do_rbac/do_user.js @@ -53,12 +53,14 @@ function _showUser(opts, cb) { assert.object(opts.cli, 'opts.cli'); assert.string(opts.id, 'opts.id'); assert.optionalBool(opts.roles, 'opts.roles'); + assert.optionalBool(opts.keys, 'opts.keys'); assert.func(cb, 'cb'); var cli = opts.cli; cli.tritonapi.getUser({ id: opts.id, - roles: opts.roles + roles: opts.roles, + keys: opts.keys }, function onUser(err, user) { if (err) { return cb(err); @@ -68,8 +70,20 @@ function _showUser(opts, cb) { console.log(JSON.stringify(user)); } else { Object.keys(user).forEach(function (key) { + if (key === 'keys') { + return; + } console.log('%s: %s', key, user[key]); }); + 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'); + } + }); + } } cb(); }); @@ -397,6 +411,7 @@ function do_user(subcmd, opts, args, cb) { cli: this.top, id: args[0], roles: opts.roles || opts.membership, + keys: opts.keys, json: opts.json }, cb); break; @@ -436,7 +451,7 @@ do_user.options = [ { names: ['roles', 'r'], type: 'bool', - help: 'Include "roles" and "default_roles" this user has.' + help: 'Include "roles" and "default_roles" fields for this user.' }, { names: ['membership'], @@ -446,6 +461,11 @@ do_user.options = [ 'node-smartdc.', hidden: true }, + { + names: ['keys', 'k'], + type: 'bool', + help: 'Include SSH keys (the "keys" field) for this user.' + }, { names: ['yes', 'y'], type: 'bool', diff --git a/lib/do_rbac/index.js b/lib/do_rbac/index.js index 784af4b..c53d9e6 100644 --- a/lib/do_rbac/index.js +++ b/lib/do_rbac/index.js @@ -50,4 +50,7 @@ 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'); + module.exports = RbacCLI; diff --git a/lib/tritonapi.js b/lib/tritonapi.js index 2846029..cd423a5 100644 --- a/lib/tritonapi.js +++ b/lib/tritonapi.js @@ -596,6 +596,8 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) { * - id {UUID|String} The user ID (a UUID), login or short id. * - 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 + * request) retrieve the `keys` for this user. Default is false. * @param {Function} callback of the form `function (err, user)` */ TritonApi.prototype.getUser = function getUser(opts, cb) { @@ -603,6 +605,7 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { assert.object(opts, 'opts'); assert.string(opts.id, 'opts.id'); assert.optionalBool(opts.roles, 'opts.roles'); + assert.optionalBool(opts.keys, 'opts.keys'); assert.func(cb, 'cb'); /* @@ -705,6 +708,21 @@ TritonApi.prototype.getUser = function getUser(opts, cb) { next(); } }); + }, + + function getKeys(ctx, next) { + if (!opts.keys) { + next(); + return; + } + self.cloudapi.listUserKeys({id: ctx.user.id}, function (err, keys) { + if (err) { + next(err); + return; + } + ctx.user.keys = keys; + next(); + }); } ]}, function (err) {