joyent/node-triton#54 a start at 'triton rbac info', add 'triton rbac instance-role-tags'
This commit is contained in:
parent
74b8f3e42e
commit
4e45e4061f
@ -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
|
||||
|
@ -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) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* <http://apidocs.joyent.com/cloudapi/#SetRoleTags>
|
||||
* 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: <resource url>,
|
||||
* 'role-tag': <array of added role tags>}`.
|
||||
* @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) {
|
||||
|
272
lib/do_rbac/do_info.js
Normal file
272
lib/do_rbac/do_info.js
Normal file
@ -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 [<options>]\n' +
|
||||
'\n' +
|
||||
'{{options}}'
|
||||
/* END JSSTYLED */
|
||||
);
|
||||
|
||||
|
||||
|
||||
module.exports = do_info;
|
582
lib/do_rbac/do_role_tags.js
Normal file
582
lib/do_rbac/do_role_tags.js
Normal file
@ -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 <Enter> 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
|
||||
};
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user