joyent/node-triton#54 first pass at 'triton rbac role' and 'triton rbac roles'
This commit is contained in:
parent
1652662e2c
commit
dd95ab5f4a
@ -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
|
||||
|
122
lib/cloudapi2.js
122
lib/cloudapi2.js
@ -713,7 +713,6 @@ CloudApi.prototype.getUser = function getUser(opts, cb) {
|
||||
/**
|
||||
* <http://apidocs.joyent.com/cloudapi/#CreateUser>
|
||||
*
|
||||
* @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) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* <http://apidocs.joyent.com/cloudapi/#ListRoles>
|
||||
*
|
||||
* @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);
|
||||
};
|
||||
|
||||
/**
|
||||
* <http://apidocs.joyent.com/cloudapi/#GetRole>
|
||||
*
|
||||
* @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);
|
||||
};
|
||||
|
||||
/**
|
||||
* <http://apidocs.joyent.com/cloudapi/#CreateRole>
|
||||
*
|
||||
* @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);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* <http://apidocs.joyent.com/cloudapi/#UpdateRole>
|
||||
*
|
||||
* @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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* <http://apidocs.joyent.com/cloudapi/#DeleteRole>
|
||||
*
|
||||
* @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
|
||||
|
||||
|
518
lib/do_rbac/do_role.js
Normal file
518
lib/do_rbac/do_role.js
Normal file
@ -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 <Enter> 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;
|
125
lib/do_rbac/do_roles.js
Normal file
125
lib/do_rbac/do_roles.js
Normal file
@ -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 [<options>]\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;
|
@ -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);
|
||||
});
|
||||
|
@ -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;
|
||||
|
135
lib/tritonapi.js
135
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
|
||||
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user