joyent/node-triton#54 'triton rbac apply --dev-create-keys-and-profiles'
This commit is contained in:
parent
6918fb93f7
commit
82443e2d67
@ -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.
|
||||
|
@ -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
|
@ -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:
|
||||
|
@ -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');
|
||||
|
125
lib/rbac.js
125
lib/rbac.js
@ -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));
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user