joyent/node-triton#54 Complete first pass at 'triton rbac user' and 'triton rbac users'
This commit is contained in:
parent
8a46d23268
commit
1652662e2c
@ -10,6 +10,8 @@
|
|||||||
- `triton profiles` now shows the optional `user` fields.
|
- `triton profiles` now shows the optional `user` fields.
|
||||||
- A (currently experimental and hidden) `triton rbac ...` command to
|
- A (currently experimental and hidden) `triton rbac ...` command to
|
||||||
house RBAC CLI functionality.
|
house RBAC CLI functionality.
|
||||||
|
- `triton rbac users` to list all users.
|
||||||
|
- `triton rbac user ...` to show, create, edit and delete users.
|
||||||
|
|
||||||
|
|
||||||
## 2.1.4
|
## 2.1.4
|
||||||
|
117
lib/cloudapi2.js
117
lib/cloudapi2.js
@ -710,6 +710,123 @@ CloudApi.prototype.getUser = function getUser(opts, cb) {
|
|||||||
this._passThrough(endpoint, {membership: opts.membership}, cb);
|
this._passThrough(endpoint, {membership: opts.membership}, cb);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <http://apidocs.joyent.com/cloudapi/#CreateUser>
|
||||||
|
*
|
||||||
|
* @param {String} account (optional) the login name of the account.
|
||||||
|
* @param {Object} opts (object) user object containing:
|
||||||
|
* - {String} login (required) for your user.
|
||||||
|
* - {String} password (required) for the user.
|
||||||
|
* - {String} email (required) 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.
|
||||||
|
* @param {Function} cb of the form `function (err, user, res)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.createUser = function createUser(opts, cb) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.string(opts.login, 'opts.login');
|
||||||
|
assert.string(opts.password, 'opts.password');
|
||||||
|
assert.string(opts.email, 'opts.email');
|
||||||
|
// XXX strict on inputs
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
login: opts.login,
|
||||||
|
password: opts.password,
|
||||||
|
email: opts.email,
|
||||||
|
companyName: opts.companyName,
|
||||||
|
firstName: opts.firstName,
|
||||||
|
lastName: opts.lastName,
|
||||||
|
address: opts.address,
|
||||||
|
postalCode: opts.postalCode,
|
||||||
|
city: opts.city,
|
||||||
|
state: opts.state,
|
||||||
|
country: opts.country,
|
||||||
|
phone: opts.phone
|
||||||
|
};
|
||||||
|
|
||||||
|
this._request({
|
||||||
|
method: 'POST',
|
||||||
|
path: format('/%s/users', this.account),
|
||||||
|
data: data
|
||||||
|
}, function (err, req, res, body) {
|
||||||
|
cb(err, body, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <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.
|
||||||
|
* @param {Function} cb of the form `function (err, user, res)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.updateUser = function updateUser(opts, cb) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.string(opts.id, 'opts.id');
|
||||||
|
// XXX strict on inputs
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
var update = {
|
||||||
|
email: opts.email,
|
||||||
|
companyName: opts.companyName,
|
||||||
|
firstName: opts.firstName,
|
||||||
|
lastName: opts.lastName,
|
||||||
|
address: opts.address,
|
||||||
|
postalCode: opts.postalCode,
|
||||||
|
city: opts.city,
|
||||||
|
state: opts.state,
|
||||||
|
country: opts.country,
|
||||||
|
phone: opts.phone
|
||||||
|
};
|
||||||
|
|
||||||
|
this._request({
|
||||||
|
method: 'POST',
|
||||||
|
path: format('/%s/users/%s', this.account, opts.id),
|
||||||
|
data: update
|
||||||
|
}, function (err, req, res, body) {
|
||||||
|
cb(err, body, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <http://apidocs.joyent.com/cloudapi/#DeleteUser>
|
||||||
|
*
|
||||||
|
* @param {Object} opts (object) user object containing:
|
||||||
|
* - {String} id (required) for your user.
|
||||||
|
* @param {Function} cb of the form `function (err, user, res)`
|
||||||
|
*/
|
||||||
|
CloudApi.prototype.deleteUser = function deleteUser(opts, cb) {
|
||||||
|
assert.object(opts, 'opts');
|
||||||
|
assert.string(opts.id, 'opts.id');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
|
||||||
|
this._request({
|
||||||
|
method: 'DELETE',
|
||||||
|
path: format('/%s/users/%s', this.account, opts.id)
|
||||||
|
}, function (err, req, res, body) {
|
||||||
|
cb(err, body, res);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Exports
|
// --- Exports
|
||||||
|
@ -560,6 +560,9 @@ function promptEnter(prompt, cb) {
|
|||||||
* cb(new Error('value is not a number'));
|
* cb(new Error('value is not a number'));
|
||||||
* cb(); // value is fine as is
|
* cb(); // value is fine as is
|
||||||
* cb(null, Math.floor(Number(value))); // manip to a floored int
|
* cb(null, Math.floor(Number(value))); // manip to a floored int
|
||||||
|
* - field.required {Boolean} Optional. If `field.validate` is not
|
||||||
|
* given, `required=true` will provide a validate func that requires
|
||||||
|
* a value.
|
||||||
* @params cb {Function} `function (err, value)`
|
* @params cb {Function} `function (err, value)`
|
||||||
* If the user aborted, the `err` will be whatever the [read
|
* If the user aborted, the `err` will be whatever the [read
|
||||||
* package](https://www.npmjs.com/package/read) returns, i.e. a
|
* package](https://www.npmjs.com/package/read) returns, i.e. a
|
||||||
@ -567,22 +570,36 @@ function promptEnter(prompt, cb) {
|
|||||||
*/
|
*/
|
||||||
function promptField(field, cb) {
|
function promptField(field, cb) {
|
||||||
var wrap = wordwrap(Math.min(process.stdout.columns, 78));
|
var wrap = wordwrap(Math.min(process.stdout.columns, 78));
|
||||||
|
|
||||||
|
var validate = field.validate;
|
||||||
|
if (!validate && field.required) {
|
||||||
|
validate = function (value, validateCb) {
|
||||||
|
if (!value) {
|
||||||
|
validateCb(new Error(format('A value for "%s" is required.',
|
||||||
|
field.key)));
|
||||||
|
} else {
|
||||||
|
validateCb();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function attempt(next) {
|
function attempt(next) {
|
||||||
read({
|
read({
|
||||||
// read/readline prompting messes up width with ANSI codes here.
|
// read/readline prompting messes up width with ANSI codes here.
|
||||||
prompt: field.key + ':',
|
prompt: field.key + ':',
|
||||||
default: field.default,
|
default: field.default,
|
||||||
|
silent: field.password,
|
||||||
edit: true
|
edit: true
|
||||||
}, function (err, result, isDefault) {
|
}, function (err, result, isDefault) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
}
|
}
|
||||||
var value = result.trim();
|
var value = result.trim();
|
||||||
if (!field.validate) {
|
if (!validate) {
|
||||||
return cb(null, value);
|
return cb(null, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
field.validate(value, function (validationErr, newValue) {
|
validate(value, function (validationErr, newValue) {
|
||||||
if (validationErr) {
|
if (validationErr) {
|
||||||
console.log(ansiStylize(
|
console.log(ansiStylize(
|
||||||
wrap(validationErr.message), 'red'));
|
wrap(validationErr.message), 'red'));
|
||||||
@ -597,7 +614,9 @@ function promptField(field, cb) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field.desc) {
|
||||||
console.log(ansiStylize(wrap(field.desc), 'bold'));
|
console.log(ansiStylize(wrap(field.desc), 'bold'));
|
||||||
|
}
|
||||||
attempt();
|
attempt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
var assert = require('assert-plus');
|
var assert = require('assert-plus');
|
||||||
var format = require('util').format;
|
var format = require('util').format;
|
||||||
var fs = require('fs');
|
var fs = require('fs');
|
||||||
var read = require('read');
|
|
||||||
var strsplit = require('strsplit');
|
var strsplit = require('strsplit');
|
||||||
var sshpk = require('sshpk');
|
var sshpk = require('sshpk');
|
||||||
var tilde = require('tilde-expansion');
|
var tilde = require('tilde-expansion');
|
||||||
|
@ -10,21 +10,55 @@
|
|||||||
* `triton rbac user ...`
|
* `triton rbac user ...`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var assert = require('assert-plus');
|
||||||
|
var format = require('util').format;
|
||||||
|
var fs = require('fs');
|
||||||
|
var strsplit = require('strsplit');
|
||||||
|
var vasync = require('vasync');
|
||||||
|
|
||||||
|
var common = require('../common');
|
||||||
var errors = require('../errors');
|
var errors = require('../errors');
|
||||||
|
|
||||||
|
|
||||||
|
var UPDATABLE_USER_FIELDS = [
|
||||||
|
{key: 'email', required: true},
|
||||||
|
{key: 'firstName'},
|
||||||
|
{key: 'lastName'},
|
||||||
|
{key: 'companyName'},
|
||||||
|
{key: 'address'},
|
||||||
|
{key: 'postalCode'},
|
||||||
|
{key: 'city'},
|
||||||
|
{key: 'state'},
|
||||||
|
{key: 'country'},
|
||||||
|
{key: 'phone'}
|
||||||
|
];
|
||||||
|
|
||||||
function do_user(subcmd, opts, args, cb) {
|
var CREATE_USER_FIELDS = [
|
||||||
if (opts.help) {
|
{key: 'login', required: true},
|
||||||
this.do_help('help', {}, [subcmd], cb);
|
{key: 'password', password: true, required: true},
|
||||||
return;
|
{key: 'email', required: true},
|
||||||
} else if (args.length !== 1) {
|
{key: 'firstName'},
|
||||||
return cb(new errors.UsageError('incorrect number of args'));
|
{key: 'lastName'},
|
||||||
}
|
{key: 'companyName'},
|
||||||
|
{key: 'address'},
|
||||||
|
{key: 'postalCode'},
|
||||||
|
{key: 'city'},
|
||||||
|
{key: 'state'},
|
||||||
|
{key: 'country'},
|
||||||
|
{key: 'phone'}
|
||||||
|
];
|
||||||
|
|
||||||
this.top.tritonapi.getUser({
|
|
||||||
id: args[0],
|
function _showUser(opts, cb) {
|
||||||
roles: opts.roles || opts.membership
|
assert.object(opts.cli, 'opts.cli');
|
||||||
|
assert.string(opts.id, 'opts.id');
|
||||||
|
assert.optionalBool(opts.roles, 'opts.roles');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
var cli = opts.cli;
|
||||||
|
|
||||||
|
cli.tritonapi.getUser({
|
||||||
|
id: opts.id,
|
||||||
|
roles: opts.roles
|
||||||
}, function onUser(err, user) {
|
}, function onUser(err, user) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return cb(err);
|
return cb(err);
|
||||||
@ -33,12 +67,362 @@ function do_user(subcmd, opts, args, cb) {
|
|||||||
if (opts.json) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(user));
|
console.log(JSON.stringify(user));
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify(user, null, 4));
|
Object.keys(user).forEach(function (key) {
|
||||||
|
console.log('%s: %s', key, user[key]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cb();
|
cb();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _yamlishFromUser(user) {
|
||||||
|
assert.object(user, 'user');
|
||||||
|
|
||||||
|
var lines = [];
|
||||||
|
UPDATABLE_USER_FIELDS.forEach(function (field) {
|
||||||
|
lines.push(format('%s: %s', field.key, user[field.key] || ''));
|
||||||
|
});
|
||||||
|
return lines.join('\n') + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _userFromYamlish(yamlish) {
|
||||||
|
assert.string(yamlish, 'yamlish');
|
||||||
|
|
||||||
|
var user = {};
|
||||||
|
var lines = yamlish.split(/\n/g);
|
||||||
|
lines.forEach(function (line) {
|
||||||
|
var commentIdx = line.indexOf('#');
|
||||||
|
if (commentIdx !== -1) {
|
||||||
|
line = line.slice(0, commentIdx);
|
||||||
|
}
|
||||||
|
line = line.trim();
|
||||||
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var parts = strsplit(line, ':', 2);
|
||||||
|
var key = parts[0].trim();
|
||||||
|
var value = parts[1].trim();
|
||||||
|
user[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _editUser(opts, cb) {
|
||||||
|
assert.object(opts.cli, 'opts.cli');
|
||||||
|
assert.string(opts.id, 'opts.id');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
var cli = opts.cli;
|
||||||
|
|
||||||
|
var user;
|
||||||
|
var filename;
|
||||||
|
var origText;
|
||||||
|
|
||||||
|
function offerRetry(afterText) {
|
||||||
|
common.promptEnter(
|
||||||
|
'Press <Enter> to re-edit, Ctrl+C to abort.',
|
||||||
|
function (aborted) {
|
||||||
|
if (aborted) {
|
||||||
|
console.log('\nAborting. No change made to user.');
|
||||||
|
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 editedUser = _userFromYamlish(afterText);
|
||||||
|
editedUser.id = user.id;
|
||||||
|
|
||||||
|
if (_yamlishFromUser(editedUser) === origText) {
|
||||||
|
// This YAMLish is the closest to a canonical form we have.
|
||||||
|
console.log('No change to user');
|
||||||
|
cb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (textErr) {
|
||||||
|
console.error('Error with your changes: %s', textErr);
|
||||||
|
offerRetry(afterText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes.
|
||||||
|
cli.tritonapi.cloudapi.updateUser(editedUser, function (uErr, uu) {
|
||||||
|
if (uErr) {
|
||||||
|
console.error('Error updating user with your changes: %s',
|
||||||
|
uErr);
|
||||||
|
offerRetry(afterText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Updated user "%s"', uu.login);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cli.tritonapi.getUser({
|
||||||
|
id: opts.id,
|
||||||
|
roles: opts.roles
|
||||||
|
}, function onUser(err, user_) {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
user = user_;
|
||||||
|
filename = format('user-%s-%s.txt', cli.account, user.login);
|
||||||
|
origText = _yamlishFromUser(user);
|
||||||
|
editAttempt(origText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _deleteUsers(opts, cb) {
|
||||||
|
assert.object(opts.cli, 'opts.cli');
|
||||||
|
assert.arrayOfString(opts.ids, 'opts.ids');
|
||||||
|
assert.optionalBool(opts.yes, 'opts.yes');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
var cli = opts.cli;
|
||||||
|
|
||||||
|
if (opts.ids.length === 0) {
|
||||||
|
cb();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vasync.pipeline({funcs: [
|
||||||
|
function confirm(_, next) {
|
||||||
|
if (opts.yes) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
var msg;
|
||||||
|
if (opts.ids.length === 1) {
|
||||||
|
msg = 'Delete user "' + opts.ids[0] + '"? [y/n] ';
|
||||||
|
} else {
|
||||||
|
msg = 'Delete %d users (' + opts.ids.join(', ') + ')? [y/n] ';
|
||||||
|
|
||||||
|
}
|
||||||
|
common.promptYesNo({msg: msg}, function (answer) {
|
||||||
|
if (answer !== 'y') {
|
||||||
|
console.error('Aborting');
|
||||||
|
next(true); // early abort signal
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function deleteThem(_, next) {
|
||||||
|
vasync.forEachPipeline({
|
||||||
|
inputs: opts.ids,
|
||||||
|
func: function deleteOne(id, nextId) {
|
||||||
|
cli.tritonapi.cloudapi.deleteUser({id: id}, function (err) {
|
||||||
|
if (err) {
|
||||||
|
nextId(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Deleted user "%s"', id);
|
||||||
|
nextId();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, next);
|
||||||
|
}
|
||||||
|
]}, function (err) {
|
||||||
|
if (err === true) {
|
||||||
|
err = null;
|
||||||
|
}
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function _addUser(opts, cb) {
|
||||||
|
assert.object(opts.cli, 'opts.cli');
|
||||||
|
assert.optionalString(opts.file, 'opts.file');
|
||||||
|
assert.func(cb, 'cb');
|
||||||
|
var cli = opts.cli;
|
||||||
|
var log = cli.log;
|
||||||
|
|
||||||
|
var data;
|
||||||
|
|
||||||
|
vasync.pipeline({funcs: [
|
||||||
|
function gatherDataStdin(_, next) {
|
||||||
|
if (opts.file !== '-') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
var stdin = '';
|
||||||
|
process.stdin.resume();
|
||||||
|
process.stdin.on('data', function (chunk) {
|
||||||
|
stdin += chunk;
|
||||||
|
});
|
||||||
|
process.stdin.on('end', function () {
|
||||||
|
try {
|
||||||
|
data = JSON.parse(stdin);
|
||||||
|
} catch (err) {
|
||||||
|
log.trace({stdin: stdin}, 'invalid user JSON on stdin');
|
||||||
|
return next(new errors.TritonError(
|
||||||
|
format('invalid user JSON on stdin: %s', err)));
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function gatherDataFile(_, next) {
|
||||||
|
if (!opts.file || opts.file === '-') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
var input = fs.readFileSync(opts.file);
|
||||||
|
try {
|
||||||
|
data = JSON.parse(input);
|
||||||
|
} catch (err) {
|
||||||
|
return next(new errors.TritonError(format(
|
||||||
|
'invalid user JSON in "%s": %s', opts.file, err)));
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
function gatherDataInteractive(_, next) {
|
||||||
|
if (opts.file) {
|
||||||
|
return next();
|
||||||
|
} else if (!process.stdin.isTTY) {
|
||||||
|
return next(new errors.UsageError('cannot interactively ' +
|
||||||
|
'create a user: stdin is not a TTY'));
|
||||||
|
} else if (!process.stdout.isTTY) {
|
||||||
|
return next(new errors.UsageError('cannot interactively ' +
|
||||||
|
'create a user: stdout is not a TTY'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: confirm password
|
||||||
|
// TODO: some validation on login, email, password complexity
|
||||||
|
// TODO: retries on failure
|
||||||
|
// TODO: on failure write out to a tmp file with cmd to add it
|
||||||
|
data = {};
|
||||||
|
vasync.forEachPipeline({
|
||||||
|
inputs: CREATE_USER_FIELDS,
|
||||||
|
func: function getField(field, nextField) {
|
||||||
|
common.promptField(field, function (err, value) {
|
||||||
|
if (value) {
|
||||||
|
data[field.key] = value;
|
||||||
|
}
|
||||||
|
nextField(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, function (err) {
|
||||||
|
console.log();
|
||||||
|
next(err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function validateData(_, next) {
|
||||||
|
var missing = [];
|
||||||
|
var dataCopy = common.objCopy(data);
|
||||||
|
CREATE_USER_FIELDS.forEach(function (field) {
|
||||||
|
if (dataCopy.hasOwnProperty(field.key)) {
|
||||||
|
delete dataCopy[field.key];
|
||||||
|
} else if (field.required) {
|
||||||
|
missing.push(field.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var extra = Object.keys(dataCopy);
|
||||||
|
var issues = [];
|
||||||
|
if (missing.length) {
|
||||||
|
issues.push(format('%s missing required field%s: %s',
|
||||||
|
missing.length, (missing.length === 1 ? '' : 's'),
|
||||||
|
missing.join(', ')));
|
||||||
|
}
|
||||||
|
if (extra.length) {
|
||||||
|
issues.push(format('extraneous field%s: %s',
|
||||||
|
(extra.length === 1 ? '' : 's'), extra.join(', ')));
|
||||||
|
}
|
||||||
|
if (issues.length) {
|
||||||
|
next(new errors.TritonError(
|
||||||
|
'invalid user data: ' + issues.join('; ')));
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function createIt(_, next) {
|
||||||
|
cli.tritonapi.cloudapi.createUser(data, function (err, user) {
|
||||||
|
if (err) {
|
||||||
|
next(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('Created user "%s"', user.login);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
]}, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function do_user(subcmd, opts, args, cb) {
|
||||||
|
if (opts.help) {
|
||||||
|
this.do_help('help', {}, [subcmd], cb);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which action?
|
||||||
|
var actions = [];
|
||||||
|
if (opts.add) { actions.push('add'); }
|
||||||
|
if (opts.edit) { actions.push('edit'); }
|
||||||
|
if (opts['delete']) { actions.push('delete'); }
|
||||||
|
var action;
|
||||||
|
if (actions.length === 0) {
|
||||||
|
action = 'show';
|
||||||
|
} else if (actions.length > 1) {
|
||||||
|
return cb(new errors.UsageError(
|
||||||
|
'only one action option may be used at once'));
|
||||||
|
} else {
|
||||||
|
action = actions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arg count validation.
|
||||||
|
if (args.length > 1) {
|
||||||
|
return cb(new errors.UsageError('too many arguments'));
|
||||||
|
} else if (args.length === 0 &&
|
||||||
|
['show', 'edit'].indexOf(action) !== -1)
|
||||||
|
{
|
||||||
|
return cb(new errors.UsageError('USER argument is required'));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'show':
|
||||||
|
_showUser({
|
||||||
|
cli: this.top,
|
||||||
|
id: args[0],
|
||||||
|
roles: opts.roles || opts.membership,
|
||||||
|
json: opts.json
|
||||||
|
}, cb);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
// TODO: support `triton rbac user trent -e companyName=Tuna` k=v args
|
||||||
|
_editUser({
|
||||||
|
cli: this.top,
|
||||||
|
id: args[0]
|
||||||
|
}, cb);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
_deleteUsers({
|
||||||
|
cli: this.top,
|
||||||
|
ids: args,
|
||||||
|
yes: opts.yes
|
||||||
|
}, cb);
|
||||||
|
break;
|
||||||
|
case 'add':
|
||||||
|
_addUser({cli: this.top, file: args[0]}, cb);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return cb(new errors.InternalError('unknown action: ' + action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
do_user.options = [
|
do_user.options = [
|
||||||
{
|
{
|
||||||
names: ['help', 'h'],
|
names: ['help', 'h'],
|
||||||
@ -62,20 +446,54 @@ do_user.options = [
|
|||||||
'for backward compat with `sdc-user get --membership ...` from ' +
|
'for backward compat with `sdc-user get --membership ...` from ' +
|
||||||
'node-smartdc.',
|
'node-smartdc.',
|
||||||
hidden: true
|
hidden: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['yes', 'y'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Answer yes to confirmations, e.g. confirmation of deletion.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: 'Action Options'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['edit', 'e'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Edit the named user in your $EDITOR.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['add', 'a'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Add a new user.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
names: ['delete', 'd'],
|
||||||
|
type: 'bool',
|
||||||
|
help: 'Delete the named user.'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
do_user.help = (
|
do_user.help = [
|
||||||
/* BEGIN JSSTYLED */
|
/* BEGIN JSSTYLED */
|
||||||
'Get an RBAC user.\n' +
|
'Show, add, edit and delete RBAC users.',
|
||||||
'\n' +
|
'',
|
||||||
'Usage:\n' +
|
'Usage:',
|
||||||
' {{name}} user [<options>] ID|LOGIN|SHORT-ID\n' +
|
' {{name}} user USER # show user USER',
|
||||||
'\n' +
|
' {{name}} user -e|--edit USER # edit user USER in $EDITOR',
|
||||||
'{{options}}' +
|
' {{name}} user -d|--delete [USER...] # delete user USER',
|
||||||
'\n' +
|
'',
|
||||||
'Note: Currently this dumps indented JSON by default. That might change\n' +
|
' {{name}} user -a|--add [FILE]',
|
||||||
'in the future. Use "-j" to explicitly get JSON output.\n'
|
' # Add a new user. FILE must be a file path to a JSON file',
|
||||||
|
' # with the user data or "-" to pass the user in on stdin.',
|
||||||
|
' # Or exclude FILE to interactively add.',
|
||||||
|
'',
|
||||||
|
'{{options}}',
|
||||||
|
'Where "USER" is a full user "id", the user "login" name or a "shortid", i.e.',
|
||||||
|
'an id prefix.',
|
||||||
|
'',
|
||||||
|
'Fields for creating a user:',
|
||||||
|
CREATE_USER_FIELDS.map(function (field) {
|
||||||
|
return ' ' + field.key + (field.required ? ' (required)' : '');
|
||||||
|
}).join('\n')
|
||||||
/* END JSSTYLED */
|
/* END JSSTYLED */
|
||||||
);
|
].join('\n');
|
||||||
|
|
||||||
module.exports = do_user;
|
module.exports = do_user;
|
||||||
|
Reference in New Issue
Block a user