joyent/node-triton#54 'triton rbac apply'
This commit is contained in:
parent
00cdb81287
commit
7c8554bf14
@ -18,10 +18,13 @@
|
||||
- `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,image,network,package,}role-tags ...` to list
|
||||
and manage role tags on each of those resources.
|
||||
- `triton rbac info` will dump a summary of the full current RBAC
|
||||
state. This command is still in development.
|
||||
- `triton rbac apply` will synchronize a local RBAC config (by default it
|
||||
looks for "./rbac.json") to live RBAC state. Current the RBAC config
|
||||
file format is undocumented. See "examples/rbac-\*" for examples.
|
||||
- #55 Update of smartdc-auth/sshpk deps, removal of duplicated code for
|
||||
composing Authorization headers
|
||||
|
||||
|
27
examples/rbac-simple/README.md
Normal file
27
examples/rbac-simple/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
*Caveat*: All `triton rbac ...` support is experimental.
|
||||
|
||||
This directly holds a super simple example Triton RBAC Profile for a mythical
|
||||
"Simple Corp.", with `triton` CLI examples showing how to use it for RBAC.
|
||||
|
||||
Our Simple corporation will create an "rbactestsimple" Triton account and
|
||||
use RBAC to manage its users, roles, etc. It has two users:
|
||||
|
||||
- emma: Should have full access, to everything.
|
||||
- bert: Should only have read access, again to everything.
|
||||
|
||||
We want an RBAC config that allows appropriate access for all the employees
|
||||
and tooling. Roughly we'll break that into roles as follows:
|
||||
|
||||
- Role `admin`. Complete access to the API. Only used by "emma" when, e.g.,
|
||||
updating RBAC configuration itself.
|
||||
- Role `ops`. Full access, except to RBAC configuration updates.
|
||||
- Role `read`. Read-only access to compute resources.
|
||||
|
||||
See "rbac.json" where we encode all this.
|
||||
|
||||
The `triton rbac apply` command can work with a JSON config file (and
|
||||
optionally separate user public ssh key files) to create and maintain a
|
||||
Triton RBAC configuration. In our example this will be:
|
||||
|
||||
triton rbac apply # defaults to looking at "./rbac.json"
|
||||
|
1
examples/rbac-simple/rbac-user-keys/emma.pub
Normal file
1
examples/rbac-simple/rbac-user-keys/emma.pub
Normal file
@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDeDi6WjD2EOe+N8tkJXsDmjMii4qrJfEm4TX56Yezrjxr5QBSoPRcahvX+n352LYbkid+23lNrqGdBDlhracbFHoduWUetStRGoQWk52c7Piu13ETqWwoqxSlV9L9Ajb9h78AhBkTdtMlGp/0s6yREsjirUYyi/sGfHaj4WJU2Iqh3Ig1VsOcSUmm5npYC19UF+5hbEYTDEidkfRpjdDXA5EGTTce9ytsELOvFQaI/UuwXIS8Xd+1Eg/a+T3eFkx8VN8lAu067JfJNh8baaJcf5Bim3VcaTVDaYshwHJ0OKRxwvV/PqHLrEEbUA2zdN1oUY+Wyj/DQYdijCO8xGT41cZiJDINxfy0nJLEQxpfsnJw1QhRujezyekumdveQf0SoXZtUiBnWF0E/tMgMdQ8j6fFLUGgu08hIHZU0keIYUV1bAvJJhxJM58wemjmCch0BdZd0bBbujxeicFO+N1comeZjKhpLHQHwrQhRLu+oZgI41g4zNk+8lUuy6yI3pYdD+ThAsKrV34KpPCSIVA9KbLb4uIxZzpc41uh4AT6Xu1raXeqRn1ERW3XD3L2dmNaO444iEPgNdEzG7TG6Is172GOFZT75SkkdCV7UlsPFD0O7UXoDD9rdGjoi+LmIyX4MrTLXctOalNyXV9g+RiPETOfCz4dbH6cZjq/V6NdMBw== emma
|
43
examples/rbac-simple/rbac.json
Normal file
43
examples/rbac-simple/rbac.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"users": [
|
||||
{ "login": "emma", "email": "emma@simple.example.com", "keys": "rbac-user-keys" },
|
||||
{ "login": "bert", "email": "bert@simple.example.com" }
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"name": "admin",
|
||||
"default_members": [],
|
||||
"members": ["emma"],
|
||||
"policies": ["policy-admin"]
|
||||
},
|
||||
{
|
||||
"name": "ops",
|
||||
"default_members": ["emma"],
|
||||
"members": ["emma"],
|
||||
"policies": ["policy-full"]
|
||||
},
|
||||
{
|
||||
"name": "read",
|
||||
"default_members": ["bert", "emma"],
|
||||
"members": ["bert", "emma"],
|
||||
"policies": ["policy-readonly"]
|
||||
}
|
||||
],
|
||||
"policies": [
|
||||
{
|
||||
"name": "policy-admin",
|
||||
"description": "full access",
|
||||
"rules": ["CAN *"]
|
||||
},
|
||||
{
|
||||
"name": "policy-full",
|
||||
"description": "full access, except rbac",
|
||||
"rules": ["CAN compute:*"]
|
||||
},
|
||||
{
|
||||
"name": "policy-readonly",
|
||||
"description": "read-only access",
|
||||
"rules": ["CAN compute:Get*"]
|
||||
}
|
||||
]
|
||||
}
|
@ -760,17 +760,19 @@ CloudApi.prototype.createUser = function createUser(opts, cb) {
|
||||
* <http://apidocs.joyent.com/cloudapi/#UpdateUser>
|
||||
*
|
||||
* @param {Object} opts (object) user object containing:
|
||||
* - {String} id (required) for your user.
|
||||
* - {String} email (optional) for the user.
|
||||
* - {String} companyName (optional) for the user.
|
||||
* - {String} firstName (optional) for the user.
|
||||
* - {String} lastName (optional) for the user.
|
||||
* - {String} address (optional) for the user.
|
||||
* - {String} postalCode (optional) for the user.
|
||||
* - {String} city (optional) for the user.
|
||||
* - {String} state (optional) for the user.
|
||||
* - {String} country (optional) for the user.
|
||||
* - {String} phone (optional) for the user.
|
||||
* - {String} id (required) for your user. This can be either the user
|
||||
* login, or the user id (UUID)
|
||||
* - {String} login (optional)
|
||||
* - {String} email (optional)
|
||||
* - {String} companyName (optional)
|
||||
* - {String} firstName (optional)
|
||||
* - {String} lastName (optional)
|
||||
* - {String} address (optional)
|
||||
* - {String} postalCode (optional)
|
||||
* - {String} city (optional)
|
||||
* - {String} state (optional)
|
||||
* - {String} country (optional)
|
||||
* - {String} phone (optional)
|
||||
* @param {Function} cb of the form `function (err, user, res)`
|
||||
*/
|
||||
CloudApi.prototype.updateUser = function updateUser(opts, cb) {
|
||||
@ -780,6 +782,7 @@ CloudApi.prototype.updateUser = function updateUser(opts, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
var update = {
|
||||
login: opts.login,
|
||||
email: opts.email,
|
||||
companyName: opts.companyName,
|
||||
firstName: opts.firstName,
|
||||
@ -837,7 +840,7 @@ CloudApi.prototype.listUserKeys = function listUserKeys(opts, cb) {
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
var endpoint = format('/%s/users/%s/keys', this.account, opts.userId);
|
||||
this._passThrough(endpoint, opts, cb);
|
||||
this._passThrough(endpoint, {}, cb);
|
||||
};
|
||||
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var child_process = require('child_process');
|
||||
var crypto = require('crypto');
|
||||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var path = require('path');
|
||||
@ -502,7 +503,7 @@ function promptYesNo(opts_, cb) {
|
||||
case '\n':
|
||||
case '\r':
|
||||
case '\u0004':
|
||||
// They've finished typing their answer
|
||||
// EOT. They've finished typing their answer
|
||||
postInput();
|
||||
var answer = input.toLowerCase();
|
||||
if (answer === '' && opts.default) {
|
||||
@ -517,12 +518,23 @@ function promptYesNo(opts_, cb) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case '\u0003':
|
||||
// Ctrl C
|
||||
case '\u0003': // Ctrl C
|
||||
postInput();
|
||||
finish(false);
|
||||
break;
|
||||
case '\u007f': // DEL
|
||||
input = input.slice(0, -1);
|
||||
stdout.clearLine();
|
||||
stdout.cursorTo(0);
|
||||
stdout.write(opts.msg);
|
||||
stdout.write(input);
|
||||
break;
|
||||
default:
|
||||
// Rule out special ASCII chars.
|
||||
var code = ch.charCodeAt(0);
|
||||
if (0 <= code && code <= 31) {
|
||||
break;
|
||||
}
|
||||
// More plaintext characters
|
||||
stdout.write(ch);
|
||||
input += ch;
|
||||
@ -717,6 +729,30 @@ function chomp(s) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Generate a random password of the specified length (default 20 chars)
|
||||
* using ASCII printable, non-space chars: ASCII 33-126 (inclusive).
|
||||
*
|
||||
* No idea if this is crypto-sound. Doubt it.
|
||||
*/
|
||||
function generatePassword(opts) {
|
||||
assert.optionalObject(opts, 'opts');
|
||||
opts = opts || {};
|
||||
assert.optionalNumber(opts.len, 'opts.len');
|
||||
|
||||
var buf = crypto.randomBytes(opts.len || 20);
|
||||
var min = 33;
|
||||
var max = 126;
|
||||
var chars = [];
|
||||
for (var i = 0; i < buf.length; i++) {
|
||||
var num = Math.round(((buf[i] / 0xff) * (max - min)) + min);
|
||||
chars.push(String.fromCharCode(num));
|
||||
}
|
||||
|
||||
return chars.join('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
//---- exports
|
||||
|
||||
@ -743,6 +779,7 @@ module.exports = {
|
||||
editInEditor: editInEditor,
|
||||
ansiStylize: ansiStylize,
|
||||
indent: indent,
|
||||
chomp: chomp
|
||||
chomp: chomp,
|
||||
generatePassword: generatePassword
|
||||
};
|
||||
// vim: set softtabstop=4 shiftwidth=4:
|
||||
|
137
lib/do_rbac/do_apply.js
Normal file
137
lib/do_rbac/do_apply.js
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 apply ...`
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var rbac = require('../rbac');
|
||||
|
||||
var ansiStylize = common.ansiStylize;
|
||||
|
||||
|
||||
function do_apply(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 context = {
|
||||
log: this.log,
|
||||
tritonapi: this.top.tritonapi,
|
||||
cloudapi: this.top.tritonapi.cloudapi,
|
||||
rbacConfigPath: opts.file || './rbac.json',
|
||||
rbacDryRun: opts.dry_run
|
||||
};
|
||||
vasync.pipeline({arg: context, funcs: [
|
||||
rbac.loadRbacConfig,
|
||||
rbac.loadRbacState,
|
||||
rbac.createRbacUpdatePlan,
|
||||
function confirmApply(ctx, next) {
|
||||
if (opts.yes || ctx.rbacUpdatePlan.length === 0) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
ctx.log.info({rbacUpdatePlan: ctx.rbacUpdatePlan},
|
||||
'rbacUpdatePlan');
|
||||
var p = console.log;
|
||||
p('');
|
||||
p('This will make the following RBAC config changes:');
|
||||
ctx.rbacUpdatePlan.forEach(function (c) {
|
||||
// TODO: consider having this summarize the changes, e.g.:
|
||||
// Add 5 users (bob, linda, ...)
|
||||
// Remove all 5 roles (...)
|
||||
var extra = '';
|
||||
if (c.action === 'update') {
|
||||
extra = format(' (%s)',
|
||||
Object.keys(c.diff).map(function (field) {
|
||||
return c.diff[field] + ' ' + field;
|
||||
}).join(', '));
|
||||
}
|
||||
p(' %s %s %s%s',
|
||||
{create: 'Create', 'delete': 'Delete',
|
||||
update: 'Update'}[c.action],
|
||||
c.desc || c.type,
|
||||
c.id,
|
||||
extra);
|
||||
});
|
||||
p('');
|
||||
var msg = format('Would you like to continue%s? [y/N] ',
|
||||
opts.dry_run ? ' (dry-run)' : '');
|
||||
common.promptYesNo({msg: msg, default: 'n'}, function (answer) {
|
||||
if (answer !== 'y') {
|
||||
p('Aborting update');
|
||||
return cb();
|
||||
}
|
||||
p('');
|
||||
next();
|
||||
});
|
||||
},
|
||||
rbac.executeRbacUpdatePlan
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
do_apply.options = [
|
||||
{
|
||||
names: ['help', 'h'],
|
||||
type: 'bool',
|
||||
help: 'Show this help.'
|
||||
},
|
||||
{
|
||||
names: ['dry-run', 'n'],
|
||||
type: 'bool',
|
||||
help: 'Go through the motions without applying changes.'
|
||||
},
|
||||
{
|
||||
names: ['yes', 'y'],
|
||||
type: 'bool',
|
||||
help: 'Answer "yes" to confirmation.'
|
||||
},
|
||||
{
|
||||
names: ['file', 'f'],
|
||||
type: 'string',
|
||||
helpArg: 'FILE',
|
||||
help: 'RBAC config JSON file.'
|
||||
}
|
||||
];
|
||||
|
||||
do_apply.help = [
|
||||
/* BEGIN JSSTYLED */
|
||||
'Apply an RBAC configuration.',
|
||||
'',
|
||||
'Usage:',
|
||||
' {{name}} apply [<options>]',
|
||||
'',
|
||||
'{{options}}',
|
||||
'If "--file FILE" is not specified, this defaults to using "./rbac.json".',
|
||||
'The RBAC configuration is loaded from FILE and compared to the live',
|
||||
'RBAC state (see `triton rbac info`). It then calculates necessary updates,',
|
||||
'confirms, and applies them.',
|
||||
'',
|
||||
'Warning: Currently, RBAC state updates can take a few seconds to appear',
|
||||
'as they are replicated across data centers. This can result in unexpected',
|
||||
'no-op updates with consecutive quick re-runs of this command.',
|
||||
'',
|
||||
'TODO: Document the rbac.json configuration format.'
|
||||
/* END JSSTYLED */
|
||||
].join('\n');
|
||||
|
||||
|
||||
|
||||
module.exports = do_apply;
|
@ -58,41 +58,12 @@ var vasync = require('vasync');
|
||||
|
||||
var common = require('../common');
|
||||
var errors = require('../errors');
|
||||
var rbac = require('../rbac');
|
||||
|
||||
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);
|
||||
@ -104,60 +75,23 @@ function do_info(subcmd, opts, args, cb) {
|
||||
var log = this.log;
|
||||
|
||||
var context = {
|
||||
log: this.log,
|
||||
tritonapi: this.top.tritonapi,
|
||||
cloudapi: this.top.tritonapi.cloudapi
|
||||
cloudapi: this.top.tritonapi.cloudapi,
|
||||
rbacStateAll: opts.all
|
||||
};
|
||||
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();
|
||||
},
|
||||
|
||||
rbac.loadRbacState,
|
||||
|
||||
function printInfo(ctx, next) {
|
||||
var i;
|
||||
log.trace({
|
||||
users: ctx.users,
|
||||
policies: ctx.policies,
|
||||
roles: ctx.roles
|
||||
}, 'rbac info data');
|
||||
log.trace({rbacState: ctx.rbacState}, 'rbacState');
|
||||
|
||||
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];
|
||||
console.log('users (%d):', ctx.rbacState.users.length);
|
||||
tabula.sortArrayOfObjects(ctx.rbacState.users, ['name']);
|
||||
for (i = 0; i < ctx.rbacState.users.length; i++) {
|
||||
var user = ctx.rbacState.users[i];
|
||||
|
||||
var userExtra = [];
|
||||
if (user.firstName || user.lastName) {
|
||||
@ -197,10 +131,10 @@ function do_info(subcmd, opts, args, cb) {
|
||||
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];
|
||||
console.log('roles (%d):', ctx.rbacState.roles.length);
|
||||
tabula.sortArrayOfObjects(ctx.rbacState.roles, ['name']);
|
||||
for (i = 0; i < ctx.rbacState.roles.length; i++) {
|
||||
var role = ctx.rbacState.roles[i];
|
||||
|
||||
var policyInfo;
|
||||
if (role.policies.length === 1) {
|
||||
@ -214,10 +148,10 @@ function do_info(subcmd, opts, args, cb) {
|
||||
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];
|
||||
console.log('policies (%d):', ctx.rbacState.policies.length);
|
||||
tabula.sortArrayOfObjects(ctx.rbacState.policies, ['name']);
|
||||
for (i = 0; i < ctx.rbacState.policies.length; i++) {
|
||||
var policy = ctx.rbacState.policies[i];
|
||||
var noRules = '';
|
||||
if (policy.rules.length === 0) {
|
||||
noRules = ' ' + ansiStylize('no rules', 'red');
|
||||
@ -258,7 +192,7 @@ do_info.options = [
|
||||
|
||||
do_info.help = (
|
||||
/* BEGIN JSSTYLED */
|
||||
'Print an account RBAC summary.\n' +
|
||||
'Show current RBAC state.\n' +
|
||||
'\n' +
|
||||
'Usage:\n' +
|
||||
' {{name}} info [<options>]\n' +
|
||||
|
@ -22,9 +22,11 @@ function RbacCLI(top) {
|
||||
Cmdln.call(this, {
|
||||
name: top.name + ' rbac',
|
||||
/* BEGIN JSSTYLED */
|
||||
desc: 'Role-based Access Control (RBAC) commands. *Experimental.*\n' +
|
||||
'\n' +
|
||||
'See <https://docs.joyent.com/public-cloud/rbac> for a general start.',
|
||||
desc: [
|
||||
'Role-based Access Control (RBAC) commands.',
|
||||
'See <https://docs.joyent.com/public-cloud/rbac> for a general start.',
|
||||
'**Warning: `triton rbac ...` is experimental, not well tested and in flux.**'
|
||||
].join('\n'),
|
||||
/* END JSSTYLED */
|
||||
helpOpts: {
|
||||
minHelpCol: 24 /* line up with option help */
|
||||
@ -32,6 +34,7 @@ function RbacCLI(top) {
|
||||
helpSubcmds: [
|
||||
'help',
|
||||
'info',
|
||||
'apply',
|
||||
{ group: 'RBAC Resources' },
|
||||
'users',
|
||||
'user',
|
||||
@ -59,6 +62,7 @@ RbacCLI.prototype.init = function init(opts, args, cb) {
|
||||
};
|
||||
|
||||
RbacCLI.prototype.do_info = require('./do_info');
|
||||
RbacCLI.prototype.do_apply = require('./do_apply');
|
||||
|
||||
RbacCLI.prototype.do_users = require('./do_users');
|
||||
RbacCLI.prototype.do_user = require('./do_user');
|
||||
|
758
lib/rbac.js
Normal file
758
lib/rbac.js
Normal file
@ -0,0 +1,758 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* RBAC-related support.
|
||||
*/
|
||||
|
||||
var assert = require('assert-plus');
|
||||
var format = require('util').format;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var sshpk = require('sshpk');
|
||||
var vasync = require('vasync');
|
||||
|
||||
var common = require('./common');
|
||||
var errors = require('./errors');
|
||||
|
||||
|
||||
// ---- internal support stuff
|
||||
|
||||
function _rbacStateBasics(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.rbacState.users = users;
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function listPolicies(next) {
|
||||
ctx.cloudapi.listPolicies(function (err, policies) {
|
||||
ctx.rbacState.policies = policies;
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function listRoles(next) {
|
||||
ctx.cloudapi.listRoles(function (err, roles) {
|
||||
ctx.rbacState.roles = roles;
|
||||
next(err);
|
||||
});
|
||||
}
|
||||
]}, cb);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Take `have` and `want` arrays of things (objects) and return an array of
|
||||
* "change" objects describing how to get from 'have' to 'want'.
|
||||
*
|
||||
* TODO: picture === 1000 words. Give an example.
|
||||
*
|
||||
* @param opts.type {String} The type of thing being compared.
|
||||
* @param opts.desc {String} Optional. A short descriptive phrase for this
|
||||
* thing.
|
||||
* @param opts.idField {String} The field with the unique thing id.
|
||||
* @param opts.have {Array} Array of things we have already.
|
||||
* @param opts.want {Array} Array of things we want to get to.
|
||||
* @param opts.crudChangesForCreate {Function} Optional. A function called:
|
||||
* crudChangesForCreate(wantThing)
|
||||
* to return zero or more change objects to add this thing.
|
||||
* @param opts.crudChangesForDelete {Function} Optional. A function called:
|
||||
* crudChangesForDelete(haveThing)
|
||||
* to return zero or more change objects to delete this thing.
|
||||
* @param opts.normThing {Function} Optional. A function called:
|
||||
* var normalized = normThing(thing);
|
||||
* to normalize a thing before comparing them with `crudChangesForUpdate`.
|
||||
* @param opts.crudChangesForUpdate {Function} Optional. A function called:
|
||||
* crudChangesForUpdate(haveThing, wantThing)
|
||||
* to compare to things and return zero or more change objects (of
|
||||
* action="update") for the update. If not specified, the default
|
||||
* comparison is a field-by-field "deepEqual" comparison.
|
||||
* @param opts.compareFields {Array} Optional. An alternative to specifying
|
||||
* `opts.crudChangesForUpdate` is to specify an array of field names to
|
||||
* consider. This will be used by the default crudChangesForUpdate.
|
||||
*/
|
||||
function crudChangesForThings(opts) {
|
||||
assert.object(opts, 'opts');
|
||||
assert.string(opts.type, 'opts.type');
|
||||
assert.optionalString(opts.desc, 'opts.desc');
|
||||
assert.string(opts.idField, 'opts.idField');
|
||||
assert.arrayOfObject(opts.have, 'opts.have');
|
||||
assert.arrayOfObject(opts.want, 'opts.want');
|
||||
assert.optionalFunc(opts.crudChangesForCreate, 'opts.crudChangesForCreate');
|
||||
assert.optionalFunc(opts.crudChangesForDelete, 'opts.crudChangesForDelete');
|
||||
assert.optionalFunc(opts.normThing, 'opts.normThing');
|
||||
assert.optionalFunc(opts.crudChangesForUpdate, 'opts.crudChangesForUpdate');
|
||||
assert.optionalArrayOfString(opts.compareFields, 'opts.compareFields');
|
||||
|
||||
var idField = opts.idField;
|
||||
|
||||
var differ = function (a, b) {
|
||||
try {
|
||||
assert.deepEqual(a, b);
|
||||
} catch (err) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var crudChangesForCreate = opts.crudChangesForCreate ||
|
||||
function defaultCrudChangesForCreate(wantThing_) {
|
||||
return [ {
|
||||
action: 'create',
|
||||
type: opts.type,
|
||||
desc: opts.desc,
|
||||
id: wantThing_[idField],
|
||||
wantThing: wantThing_
|
||||
} ];
|
||||
};
|
||||
|
||||
var crudChangesForDelete = opts.crudChangesForDelete ||
|
||||
function defaultCrudChangesForDelete(haveThing_) {
|
||||
return [ {
|
||||
action: 'delete',
|
||||
type: opts.type,
|
||||
desc: opts.desc,
|
||||
id: haveThing_[idField],
|
||||
haveThing: haveThing_
|
||||
} ];
|
||||
};
|
||||
|
||||
var crudChangesForUpdate = opts.crudChangesForUpdate ||
|
||||
function defaultUpdatesForThing(haveThing_, wantThing_) {
|
||||
var diff = {};
|
||||
Object.keys(haveThing_).forEach(function (field) {
|
||||
if (! wantThing_.hasOwnProperty(field)) {
|
||||
diff[field] = 'delete';
|
||||
} else if (differ(haveThing_[field], wantThing_[field])) {
|
||||
diff[field] = 'update';
|
||||
}
|
||||
});
|
||||
Object.keys(wantThing_).forEach(function (field) {
|
||||
if (! haveThing_.hasOwnProperty(field)) {
|
||||
diff[field] = 'create';
|
||||
}
|
||||
});
|
||||
if (opts.compareFields) {
|
||||
var filteredDiff = {};
|
||||
opts.compareFields.forEach(function (field) {
|
||||
if (diff.hasOwnProperty(field)) {
|
||||
filteredDiff[field] = diff[field];
|
||||
}
|
||||
});
|
||||
diff = filteredDiff;
|
||||
}
|
||||
if (Object.keys(diff).length === 0) {
|
||||
return [];
|
||||
} else {
|
||||
return [ {
|
||||
action: 'update',
|
||||
type: opts.type,
|
||||
desc: opts.desc,
|
||||
id: wantThing_[idField],
|
||||
diff: diff,
|
||||
haveThing: haveThing_,
|
||||
wantThing: wantThing_
|
||||
} ];
|
||||
}
|
||||
};
|
||||
|
||||
var normThing = opts.normThing ||
|
||||
function defaultNormThing(thing) {
|
||||
return thing;
|
||||
};
|
||||
|
||||
var haveFromId = {};
|
||||
opts.have.forEach(
|
||||
function (thing) { haveFromId[thing[idField]] = thing; });
|
||||
var wantFromId = {};
|
||||
opts.want.forEach(
|
||||
function (thing) { wantFromId[thing[idField]] = thing; });
|
||||
|
||||
// Updates and creates.
|
||||
var i, haveThing, id;
|
||||
var changes = [];
|
||||
for (i = 0; i < opts.want.length; ++i) {
|
||||
var wantThing = opts.want[i];
|
||||
id = wantThing[idField];
|
||||
haveThing = haveFromId[id];
|
||||
if (haveThing) {
|
||||
var updates = crudChangesForUpdate(
|
||||
normThing(haveThing), normThing(wantThing));
|
||||
changes = changes.concat(updates);
|
||||
} else {
|
||||
changes = changes.concat(crudChangesForCreate(wantThing));
|
||||
}
|
||||
}
|
||||
|
||||
// Deletions.
|
||||
for (i = 0; i < opts.have.length; ++i) {
|
||||
haveThing = opts.have[i];
|
||||
id = haveThing[idField];
|
||||
if (! wantFromId[id]) {
|
||||
changes = changes.concat(crudChangesForDelete(haveThing));
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---- exported functions
|
||||
|
||||
/*
|
||||
* Load a Triton RBAC config file.
|
||||
* TODO: link to docs on this.
|
||||
*
|
||||
* The result is written to `ctx.rbacConfig`.
|
||||
* This calling style is used to facilitate using this in a `vasync.pipeline`.
|
||||
*
|
||||
* @param ctx {Object} The "context".
|
||||
* @param cb {Function} `function (err)`
|
||||
*/
|
||||
function loadRbacConfig(ctx, cb) {
|
||||
assert.object(ctx, 'ctx');
|
||||
assert.string(ctx.rbacConfigPath, 'ctx.rbacConfigPath');
|
||||
assert.func(cb, 'cb');
|
||||
|
||||
vasync.pipeline({funcs: [
|
||||
function readIt(_, next) {
|
||||
fs.readFile(ctx.rbacConfigPath, function (err, data) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ctx.rbacConfig = JSON.parse(data);
|
||||
} catch (jsonErr) {
|
||||
throw new errors.TritonError(format(
|
||||
'Triton RBAC config file, %s, is not valid JSON: %s',
|
||||
ctx.rbacConfigPath, jsonErr));
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* The RBAC config format allows keys to be in a separate file
|
||||
* or dir specified with the `<user>.keys` property.
|
||||
*/
|
||||
function loadUserKeys(_, next) {
|
||||
vasync.forEachPipeline({
|
||||
inputs: ctx.rbacConfig.users || [],
|
||||
func: function loadForOneUser(user, nextUser) {
|
||||
if (!user.keys || typeof (user.keys) !== 'string') {
|
||||
nextUser();
|
||||
return;
|
||||
}
|
||||
|
||||
var keysFile = user.keys;
|
||||
try {
|
||||
var stat = fs.statSync(keysFile);
|
||||
} catch (statErr) {
|
||||
throw new errors.TritonError(format(
|
||||
'User %s keys not found in "%s": %s',
|
||||
user.login, keysFile, statErr));
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
keysFile = path.join(user.keys, user.login + '.pub');
|
||||
try {
|
||||
stat = fs.statSync(keysFile);
|
||||
} catch (statErr) {
|
||||
throw new errors.TritonError(format(
|
||||
'User %s keys not found in "%s": %s',
|
||||
user.login, keysFile, statErr));
|
||||
}
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
throw new errors.TritonError(format(
|
||||
'Expected "%s" to be a regular file', keysFile));
|
||||
}
|
||||
|
||||
var data = fs.readFileSync(keysFile, 'utf8');
|
||||
var lines = data.split(/\r?\n/g);
|
||||
user.keys = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
if (! line.trim()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
var key = sshpk.parseKey(line, 'ssh', keysFile);
|
||||
} catch (keyErr) {
|
||||
// XXX more err details, wrap with TritonError
|
||||
nextUser(keyErr);
|
||||
return;
|
||||
}
|
||||
user.keys.push({
|
||||
fingerprint: key.fingerprint('md5').toString(),
|
||||
name: key.comment || undefined,
|
||||
key: line
|
||||
});
|
||||
}
|
||||
nextUser();
|
||||
}
|
||||
}, next);
|
||||
}
|
||||
|
||||
// XXX Add JSON schema validations
|
||||
// XXX disallow password in the config file
|
||||
// XXX cannot use a login in default_members/members that
|
||||
// isn't in "users"
|
||||
// function validateConfig(_, next) {
|
||||
// }
|
||||
]}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Gather RBAC users, policies, roles and add those to the given `ctx` object.
|
||||
*
|
||||
* The result is written to `ctx.rbacState`.
|
||||
* This calling style is used to facilitate using this in a `vasync.pipeline`.
|
||||
*
|
||||
* @param ctx {Object} The "context".
|
||||
* - rbacStateAll {Boolean} Set to true to gather extra details like
|
||||
* user keys. Default false.
|
||||
* @param cb {Function} `function (err)`
|
||||
*/
|
||||
function loadRbacState(ctx, cb) {
|
||||
assert.object(ctx.cloudapi, 'ctx.cloudapi');
|
||||
assert.object(ctx.log, 'ctx.log');
|
||||
|
||||
var rbacState = ctx.rbacState = {};
|
||||
|
||||
vasync.pipeline({arg: ctx, funcs: [
|
||||
_rbacStateBasics,
|
||||
function gatherUserKeys(_, next) {
|
||||
if (ctx.rbacStateAll) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
// XXX Q! or concurrency forEachParallel
|
||||
// TODO: Optimization: could avoid getting keys for users that are
|
||||
// going to be deleted, for the `triton rbac apply` use case.
|
||||
vasync.forEachParallel({
|
||||
inputs: rbacState.users,
|
||||
func: function oneUser(user, nextUser) {
|
||||
ctx.cloudapi.listUserKeys({userId: user.id},
|
||||
function (err, userKeys) {
|
||||
user.keys = userKeys;
|
||||
nextUser(err);
|
||||
});
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
function fillInUserRoles(_, next) {
|
||||
var i;
|
||||
var userFromLogin = {};
|
||||
for (i = 0; i < rbacState.users.length; i++) {
|
||||
var user = rbacState.users[i];
|
||||
user.default_roles = [];
|
||||
user.roles = [];
|
||||
userFromLogin[user.login] = user;
|
||||
}
|
||||
for (i = 0; i < rbacState.roles.length; i++) {
|
||||
var role = rbacState.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 (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* For each of users (must be before roles), policies (must be before
|
||||
* roles), and roles we calculate updates, creates, and deletions.
|
||||
*/
|
||||
function createRbacUpdatePlan(ctx, cb) {
|
||||
assert.object(ctx.rbacConfig, 'ctx.rbacConfig');
|
||||
assert.object(ctx.rbacState, 'ctx.rbacState');
|
||||
|
||||
// XXX want option to exclude user list from the rbac config?
|
||||
// XXX handle user key updates
|
||||
// XXX guard for a full delete of all objects of any type
|
||||
// XXX guard for removal of a role that resources tagged with that
|
||||
// role could leave orphaned role tags
|
||||
// XXX can *rename* of policies and roles be supported?
|
||||
// would need 'id' to be more complex
|
||||
|
||||
var sections = [
|
||||
{
|
||||
type: 'user',
|
||||
desc: 'user',
|
||||
idField: 'login',
|
||||
have: ctx.rbacState.users || [],
|
||||
want: ctx.rbacConfig.users || [],
|
||||
/*
|
||||
* *User* update is loose: we only compare fields specified in
|
||||
* the RBAC config file. Also, user updates include updates to keys.
|
||||
*/
|
||||
crudChangesForUpdate: function userUpdates(haveUser, wantUser) {
|
||||
var updates = [];
|
||||
|
||||
var fields = [];
|
||||
Object.keys(wantUser).forEach(function (field) {
|
||||
if (field === 'keys') {
|
||||
return; // keys handled below
|
||||
}
|
||||
if (haveUser[field] !== wantUser[field]) {
|
||||
fields.push(field);
|
||||
}
|
||||
});
|
||||
if (fields.length) {
|
||||
updates.push({
|
||||
action: 'update',
|
||||
type: 'user',
|
||||
id: haveUser.login,
|
||||
fields: fields,
|
||||
haveThing: haveUser,
|
||||
wantThing: wantUser
|
||||
});
|
||||
}
|
||||
|
||||
// Note: If we get fingerprint formats other than 'md5', then
|
||||
// id comparison will have to switch to
|
||||
// `sshpk.parseKey().matches`.
|
||||
var keyChanges = crudChangesForThings({
|
||||
type: 'key',
|
||||
desc: format('user %s key', haveUser.login),
|
||||
idField: 'fingerprint',
|
||||
have: haveUser.keys || [],
|
||||
want: wantUser.keys || []
|
||||
});
|
||||
keyChanges.forEach(function (c) {
|
||||
c.user = haveUser.login;
|
||||
updates.push(c);
|
||||
});
|
||||
|
||||
return updates;
|
||||
},
|
||||
crudChangesForCreate: function userCreates(wantUser) {
|
||||
var creates = [ {
|
||||
action: 'create',
|
||||
type: 'user',
|
||||
id: wantUser.login,
|
||||
wantThing: wantUser
|
||||
} ];
|
||||
|
||||
// Add any keys.
|
||||
(wantUser.keys || []).forEach(function (key) {
|
||||
creates.push({
|
||||
action: 'create',
|
||||
type: 'key',
|
||||
desc: format('user %s key', wantUser.login),
|
||||
user: wantUser.login,
|
||||
id: key.fingerprint,
|
||||
wantThing: key
|
||||
});
|
||||
});
|
||||
|
||||
return creates;
|
||||
},
|
||||
|
||||
crudChangesForDelete: function userDeletes(haveUser) {
|
||||
var deletes = [];
|
||||
|
||||
(haveUser.keys || []).forEach(function (key) {
|
||||
deletes.push({
|
||||
action: 'delete',
|
||||
type: 'key',
|
||||
desc: format('user %s key', haveUser.login),
|
||||
user: haveUser.login,
|
||||
id: key.fingerprint,
|
||||
haveThing: key
|
||||
});
|
||||
});
|
||||
|
||||
deletes.push({
|
||||
action: 'delete',
|
||||
type: 'user',
|
||||
id: haveUser.login,
|
||||
haveThing: haveUser
|
||||
});
|
||||
|
||||
return deletes;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'policy',
|
||||
idField: 'name',
|
||||
have: ctx.rbacState.policies || [],
|
||||
want: ctx.rbacConfig.policies || [],
|
||||
compareFields: ['description', 'rules'],
|
||||
normThing: function normPolicy(policy) {
|
||||
policy.rules.sort();
|
||||
return policy;
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'role',
|
||||
idField: 'name',
|
||||
have: ctx.rbacState.roles || [],
|
||||
want: ctx.rbacConfig.roles || [],
|
||||
compareFields: ['members', 'default_members', 'policies'],
|
||||
normThing: function normRole(role) {
|
||||
role.members.sort();
|
||||
role.default_members.sort();
|
||||
role.policies.sort();
|
||||
return role;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
var changes = [];
|
||||
sections.forEach(function (section) {
|
||||
var someChanges = crudChangesForThings(section);
|
||||
changes = changes.concat(someChanges);
|
||||
});
|
||||
|
||||
ctx.rbacUpdatePlan = changes;
|
||||
cb();
|
||||
}
|
||||
|
||||
function executeRbacUpdatePlan(ctx, cb) {
|
||||
assert.object(ctx.log, 'ctx.log');
|
||||
assert.arrayOfObject(ctx.rbacUpdatePlan, 'ctx.rbacUpdatePlan');
|
||||
assert.object(ctx.cloudapi, 'ctx.cloudapi');
|
||||
assert.optionalBool(ctx.rbacDryRun, 'ctx.rbacDryRun');
|
||||
|
||||
// TODO: ctx.progress instead of console.log
|
||||
|
||||
vasync.forEachPipeline({
|
||||
inputs: ctx.rbacUpdatePlan,
|
||||
func: function executeOneChange(c, next) {
|
||||
ctx.log.info({change: c, dryRun: ctx.rbacDryRun},
|
||||
'execute rbac update change');
|
||||
var extra, delOpts, updateOpts;
|
||||
|
||||
if (ctx.rbacDryRun) {
|
||||
console.log('[dry-run] %s %s %s', c.action,
|
||||
c.desc || c.type, c.id);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (c.action + '-' + c.type) {
|
||||
case 'create-user':
|
||||
// Generate (throw away) password, if necessary.
|
||||
if (! c.wantThing.hasOwnProperty('password')) {
|
||||
c.wantThing.password = common.generatePassword();
|
||||
}
|
||||
ctx.cloudapi.createUser(c.wantThing, function (err, user) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Created user %s', c.wantThing.login);
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'update-user':
|
||||
updateOpts = {id: c.wantThing.login};
|
||||
extra = [];
|
||||
Object.keys(c.diff).forEach(function (field) {
|
||||
updateOpts[field] = c.wantThing[field];
|
||||
extra.push(format('%s=%s', field, c.wantThing[field]));
|
||||
});
|
||||
ctx.cloudapi.updateUser(updateOpts, function (err, user) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Updated user %s: %s', c.wantThing.login,
|
||||
extra.join(', '));
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'delete-user':
|
||||
delOpts = {id: c.haveThing.login};
|
||||
ctx.cloudapi.deleteUser(delOpts, function (err) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Deleted user %s', c.haveThing.login);
|
||||
next();
|
||||
});
|
||||
break;
|
||||
|
||||
case 'create-key':
|
||||
ctx.cloudapi.createUserKey({
|
||||
userId: c.user,
|
||||
key: c.wantThing.key,
|
||||
name: c.wantThing.name
|
||||
}, function (err, key) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Created user %s key %s%s', c.user,
|
||||
key.fingerprint,
|
||||
key.name ? format(' (%s)', key.name) : '');
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'update-key':
|
||||
vasync.pipeline({funcs: [
|
||||
function delKey(_, next2) {
|
||||
ctx.cloudapi.deleteUserKey({
|
||||
userId: c.user,
|
||||
fingerprint: c.haveThing.fingerprint
|
||||
}, next2);
|
||||
},
|
||||
function createKey(_, next2) {
|
||||
ctx.cloudapi.createUserKey({
|
||||
userId: c.user,
|
||||
key: c.wantThing.key,
|
||||
name: c.wantThing.name
|
||||
}, next2);
|
||||
},
|
||||
function noteIt(_, next2) {
|
||||
extra = Object.keys(c.diff).map(function (field) {
|
||||
if (field === 'key') {
|
||||
return 'key=...';
|
||||
}
|
||||
return format('%s=%s', field, c.wantThing[field]);
|
||||
});
|
||||
console.log('Updated user %s key %s: %s',
|
||||
c.user, c.wantThing.fingerprint, extra.join(', '));
|
||||
next2();
|
||||
}
|
||||
]}, next);
|
||||
break;
|
||||
case 'delete-key':
|
||||
ctx.cloudapi.deleteUserKey({
|
||||
userId: c.user,
|
||||
fingerprint: c.haveThing.fingerprint
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Deleted user %s key %s', c.user,
|
||||
c.haveThing.fingerprint);
|
||||
next();
|
||||
});
|
||||
break;
|
||||
|
||||
case 'create-policy':
|
||||
ctx.cloudapi.createPolicy(c.wantThing, function (err, poli) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Created policy %s (%s rule%s)', poli.name,
|
||||
poli.rules.length, poli.rules.length === 1 ? '' : 's');
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'update-policy':
|
||||
updateOpts = {id: c.wantThing.name};
|
||||
extra = [];
|
||||
Object.keys(c.diff).forEach(function (field) {
|
||||
updateOpts[field] = c.wantThing[field];
|
||||
// XXX This is poor for large rules update.
|
||||
extra.push(format('%s=%s', field, c.wantThing[field]));
|
||||
});
|
||||
ctx.cloudapi.updatePolicy(updateOpts, function (err, poli) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Updated policy %s: %s', poli.name,
|
||||
extra.join(', '));
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'delete-policy':
|
||||
delOpts = {id: c.haveThing.id}; // DeletePolicy requires `id`.
|
||||
ctx.cloudapi.deletePolicy(delOpts, function (err) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Deleted policy %s', c.haveThing.name);
|
||||
next();
|
||||
});
|
||||
break;
|
||||
|
||||
case 'create-role':
|
||||
ctx.cloudapi.createRole(c.wantThing, function (err, role) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Created role %s (%s members%s)',
|
||||
role.name, role.members.length,
|
||||
role.members.length === 1 ? '' : 's');
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'update-role':
|
||||
updateOpts = {id: c.wantThing.name};
|
||||
extra = [];
|
||||
Object.keys(c.diff).forEach(function (field) {
|
||||
updateOpts[field] = c.wantThing[field];
|
||||
extra.push(format('%s=%s', field, c.wantThing[field]));
|
||||
});
|
||||
ctx.cloudapi.updateRole(updateOpts, function (err, role) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Updated role %s: %s', role.name,
|
||||
extra.join(', '));
|
||||
next();
|
||||
});
|
||||
break;
|
||||
case 'delete-role':
|
||||
delOpts = {id: c.haveThing.id};
|
||||
ctx.cloudapi.deleteRole(delOpts, function (err) {
|
||||
if (err) {
|
||||
next(err);
|
||||
return;
|
||||
}
|
||||
console.log('Deleted role %s', c.haveThing.name);
|
||||
next();
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(format(
|
||||
'unknown action-type: %s-%s', c.action, c.type));
|
||||
}
|
||||
}
|
||||
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
loadRbacConfig: loadRbacConfig,
|
||||
loadRbacState: loadRbacState,
|
||||
createRbacUpdatePlan: createRbacUpdatePlan,
|
||||
executeRbacUpdatePlan: executeRbacUpdatePlan
|
||||
};
|
Reference in New Issue
Block a user