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) {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log(ansiStylize(wrap(field.desc), 'bold'));
 | 
					    if (field.desc) {
 | 
				
			||||||
 | 
					        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