joyent/node-triton#54 'triton rbac apply --dev-create-keys-and-profiles'

This commit is contained in:
Trent Mick 2015-11-23 16:57:58 -08:00
parent 6918fb93f7
commit 82443e2d67
6 changed files with 288 additions and 11 deletions

View File

@ -2,6 +2,9 @@
## 3.0.1 (not yet released)
- #54 `triton rbac apply --dev-create-keys-and-profiles` for
experimenting/dev/testing to quickly generate and add user keys and setup
Triton CLI profiles for all users in the RBAC config.
- #54 RBAC support, see <https://docs.joyent.com/public-cloud/rbac> to start.
- `triton rbac info` improvements: better help, use brackets to show
non-default roles.

View File

@ -1 +0,0 @@
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

View File

@ -766,6 +766,58 @@ function generatePassword(opts) {
}
/**
* Convenience wrapper around `child_process.exec`, mostly oriented to
* run commands using pipes w/o having to deal with logging/error handling.
*
* @param args {Object}
* - cmd {String} Required. The command to run.
* - log {Bunyan Logger} Required. Use to log details at trace level.
* - opts {Object} Optional. child_process.exec execution Options.
* @param cb {Function} `function (err, stdout, stderr)` where `err` here is
* an `errors.InternalError` wrapper around the child_process error.
*/
function execPlus(args, cb) {
assert.object(args, 'args');
assert.string(args.cmd, 'args.cmd');
assert.object(args.log, 'args.log');
assert.optionalObject(args.opts, 'args.opts');
assert.func(cb);
var cmd = args.cmd;
var execOpts = args.opts || {};
var log = args.log;
log.trace({exec: true, cmd: cmd}, 'exec start');
child_process.exec(cmd, execOpts, function execPlusCb(err, stdout, stderr) {
log.trace({exec: true, cmd: cmd, err: err, stdout: stdout,
stderr: stderr}, 'exec done');
if (err) {
var msg = format(
'exec error:\n'
+ '\tcmd: %s\n'
+ '\texit status: %s\n'
+ '\tstdout:\n%s\n'
+ '\tstderr:\n%s',
cmd, err.code, stdout.trim(), stderr.trim());
cb(new errors.InternalError({message: msg, cause: err}),
stdout, stderr);
} else {
cb(null, stdout, stderr);
}
});
}
function deepEqual(a, b) {
try {
assert.deepEqual(a, b);
} catch (err) {
return false;
}
return true;
}
//---- exports
@ -793,6 +845,8 @@ module.exports = {
ansiStylize: ansiStylize,
indent: indent,
chomp: chomp,
generatePassword: generatePassword
generatePassword: generatePassword,
execPlus: execPlus,
deepEqual: deepEqual
};
// vim: set softtabstop=4 shiftwidth=4:

View File

@ -15,6 +15,7 @@ var format = require('util').format;
var vasync = require('vasync');
var common = require('../common');
var mod_config = require('../config');
var errors = require('../errors');
var rbac = require('../rbac');
@ -22,6 +23,7 @@ var ansiStylize = common.ansiStylize;
function do_apply(subcmd, opts, args, cb) {
var self = this;
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
@ -41,6 +43,93 @@ function do_apply(subcmd, opts, args, cb) {
rbac.loadRbacConfig,
rbac.loadRbacState,
rbac.createRbacUpdatePlan,
/*
* For each user (in the target config):
* - if they don't have a key and one isn't being added in the plan
* already, then we want to create an ssh key pair and add it; and
* - add or replace the '$currprofile-user-$login' Triton CLI
* profile
*/
function devExtendKeys(ctx, next) {
if (!opts.dev_create_keys_and_profiles) {
next();
return;
}
ctx.rbacConfig.users.forEach(function devUserKey(user) {
if (user.keys && user.keys.length > 0) {
return;
}
ctx.rbacUpdatePlan.push({
action: 'generate',
type: 'key',
desc: format('user %s key', user.login),
currProfile: ctx.tritonapi.profile,
user: user.login
});
});
next();
},
function devExtendProfiles(ctx, next) {
if (!opts.dev_create_keys_and_profiles) {
next();
return;
}
var profiles = mod_config.loadAllProfiles({
configDir: self.top.configDir,
log: self.log
});
var profileFromName = {};
profiles.forEach(function (p) {
profileFromName[p.name] = p;
});
ctx.rbacConfig.users.forEach(function devUserKey(user) {
var profileName = format('%s-user-%s',
ctx.tritonapi.profile.name, user.login);
var wantThing = {
name: profileName,
url: ctx.tritonapi.profile.url,
insecure: ctx.tritonapi.profile.insecure,
account: ctx.tritonapi.profile.account,
user: user.login,
// If we are adding a key, we won't have this fingerprint
// until after executing that part of the rbacUpdatePlan.
keyId: user.keys && user.keys[0].fingerprint
};
var existing = profileFromName[profileName];
if (existing) {
// If it is the same, avoid no-op update.
if (! common.deepEqual(wantThing, existing)) {
ctx.rbacUpdatePlan.push({
action: 'update',
type: 'profile',
desc: format('user %s CLI profile', user.login),
id: profileName,
haveThing: existing,
wantThing: wantThing,
user: user.login,
configDir: self.top.configDir
});
}
} else {
ctx.rbacUpdatePlan.push({
action: 'create',
type: 'profile',
desc: format('user %s CLI profile', user.login),
id: profileName,
wantThing: wantThing,
user: user.login,
configDir: self.top.configDir
});
}
});
next();
},
function confirmApply(ctx, next) {
if (opts.yes || ctx.rbacUpdatePlan.length === 0) {
next();
@ -52,11 +141,8 @@ function do_apply(subcmd, opts, args, cb) {
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') {
if (c.action === 'update' && c.diff) {
extra = format(' (%s)',
Object.keys(c.diff).map(function (field) {
return c.diff[field] + ' ' + field;
@ -64,9 +150,9 @@ function do_apply(subcmd, opts, args, cb) {
}
p(' %s %s %s%s',
{create: 'Create', 'delete': 'Delete',
update: 'Update'}[c.action],
update: 'Update', generate: 'Generate'}[c.action],
c.desc || c.type,
c.id,
c.id || '',
extra);
});
p('');
@ -108,6 +194,12 @@ do_apply.options = [
type: 'string',
helpArg: 'FILE',
help: 'RBAC config JSON file.'
},
{
names: ['dev-create-keys-and-profiles'],
type: 'bool',
help: 'Convenient option to generate keys and Triton CLI profiles ' +
'for all users. For experimenting only. See section below.'
}
];
@ -128,6 +220,15 @@ do_apply.help = [
'as they are replicated across data centers. This can result in unexpected',
'no-op updates with consecutive quick re-runs of this command.',
'',
'The "--dev-create-keys-and-profiles" option is provided for **experimenting',
'with, developing, or testing** Triton RBAC. It will create a key and setup a ',
'Triton CLI profile for each user (named "$currprofile-user-$login"). This ',
'simplies using the CLI as that user:',
' triton -p coal-user-bob create ...',
' triton -p coal-user-sarah imgs',
'Note that proper production usage of RBAC should have the administrator',
'never seeing each user\'s private key.',
'',
'TODO: Document the rbac.json configuration format.'
/* END JSSTYLED */
].join('\n');

View File

@ -13,11 +13,15 @@
var assert = require('assert-plus');
var format = require('util').format;
var fs = require('fs');
var mkdirp = require('mkdirp');
var path = require('path');
var rimraf = require('rimraf');
var sshpk = require('sshpk');
var tilde = require('tilde-expansion');
var vasync = require('vasync');
var common = require('./common');
var mod_config = require('./config');
var errors = require('./errors');
@ -272,6 +276,7 @@ function loadRbacConfig(ctx, cb) {
var stat = fs.statSync(keysFile);
} catch (statErr) {
if (implicit) {
delete user.keys;
nextUser();
return;
}
@ -285,6 +290,7 @@ function loadRbacConfig(ctx, cb) {
stat = fs.statSync(keysFile);
} catch (statErr) {
if (implicit) {
delete user.keys;
nextUser();
return;
}
@ -295,6 +301,7 @@ function loadRbacConfig(ctx, cb) {
}
if (!stat.isFile()) {
if (implicit) {
delete user.keys;
nextUser();
return;
}
@ -567,7 +574,7 @@ function executeRbacUpdatePlan(ctx, cb) {
func: function executeOneChange(c, next) {
ctx.log.info({change: c, dryRun: ctx.rbacDryRun},
'execute rbac update change');
var extra, delOpts, updateOpts;
var extra, delOpts, updateOpts, i;
if (ctx.rbacDryRun) {
console.log('[dry-run] %s %s %s', c.action,
@ -582,7 +589,7 @@ function executeRbacUpdatePlan(ctx, cb) {
if (! c.wantThing.hasOwnProperty('password')) {
c.wantThing.password = common.generatePassword();
}
ctx.cloudapi.createUser(c.wantThing, function (err, user) {
ctx.cloudapi.createUser(c.wantThing, function (err, user_) {
if (err) {
next(err);
return;
@ -601,7 +608,7 @@ function executeRbacUpdatePlan(ctx, cb) {
updateOpts[field] = c.wantThing[field];
extra.push(format('%s=%s', field, c.wantThing[field]));
});
ctx.cloudapi.updateUser(updateOpts, function (err, user) {
ctx.cloudapi.updateUser(updateOpts, function (err, user_) {
if (err) {
next(err);
return;
@ -764,6 +771,118 @@ function executeRbacUpdatePlan(ctx, cb) {
});
break;
case 'generate-key':
console.log('Generating and adding new SSH key for user %s:',
c.user);
vasync.pipeline({arg: {}, funcs: [
function vars(ctx2, next2) {
tilde('~', function (s) {
ctx2.homeDir = s;
ctx2.keyName = format('%s user %s',
c.currProfile.name, c.user);
ctx2.keyPath = format('%s/.ssh/%s-user-%s.id_rsa',
ctx2.homeDir, c.currProfile.name, c.user);
next2();
});
},
function rmOldPrivKey(ctx2, next2) {
rimraf(ctx2.keyPath, next2);
},
function rmOldPubKey(ctx2, next2) {
rimraf(ctx2.keyPath + '.pub', next2);
},
function generateSshKey(ctx2, next2) {
var cmd = format(
'ssh-keygen -t rsa -C "%s" -f %s -b 4096',
ctx2.keyName, ctx2.keyPath);
console.log(' Generate 4096 bit RSA key: %s[.pub]',
ctx2.keyPath);
common.execPlus({cmd: cmd, log: ctx.log}, next2);
},
function loadPubKey(ctx2, next2) {
fs.readFile(ctx2.keyPath + '.pub', 'utf8',
function (err, content) {
ctx2.pubKeyContent = content;
next2();
});
},
function pubKeyCopyInRbacConfigDir(ctx2, next2) {
mkdirp(DEFAULT_RBAC_USER_KEYS_DIR, function (err) {
if (err) {
next(err);
return;
}
var configKeyPath = path.join(
DEFAULT_RBAC_USER_KEYS_DIR, c.user + '.pub');
console.log(' Copy pubkey to %s', configKeyPath);
fs.writeFile(configKeyPath, ctx2.pubKeyContent,
next2);
});
},
function addKey(ctx2, next2) {
ctx.cloudapi.createUserKey({
userId: c.user,
key: ctx2.pubKeyContent,
name: ctx2.keyName
}, function (err, key) {
if (err) {
next2(err);
return;
}
ctx2.key = key;
console.log(' Created user %s key %s%s', c.user,
key.fingerprint,
key.name ? format(' (%s)', key.name) : '');
next2();
});
},
function addKeyToRbacConfig(ctx2, next2) {
for (i = 0; i < ctx.rbacConfig.users.length; i++) {
var u = ctx.rbacConfig.users[i];
if (u.login === c.user) {
assert.ok(!u.keys,
'expect no keys on user ' + c.user);
u.keys = [ctx2.key];
break;
}
}
next2();
}
]}, next);
break;
case 'create-profile':
case 'update-profile':
if (!c.wantThing.keyId) {
// Add from the recently generated/added key.
for (i = 0; i < ctx.rbacConfig.users.length; i++) {
var user = ctx.rbacConfig.users[i];
if (user.login === c.user) {
c.wantThing.keyId = user.keys[0].fingerprint;
break;
}
}
}
try {
mod_config.validateProfile(c.wantThing);
} catch (err) {
next(err);
break;
}
try {
mod_config.saveProfileSync({
configDir: c.configDir,
profile: c.wantThing
});
} catch (err) {
return next(err);
}
next();
break;
default:
throw new Error(format(
'unknown action-type: %s-%s', c.action, c.type));

View File

@ -18,6 +18,7 @@
"read": "1.0.7",
"restify-clients": "1.1.0",
"restify-errors": "3.0.0",
"rimraf": "2.4.4",
"sshpk": "1.6.x >=1.6.2",
"smartdc-auth": "2.2.3",
"strsplit": "1.0.0",