joyent/node-triton#54 A start at RBAC support (still very early)

This commit is contained in:
Trent Mick 2015-11-03 15:40:59 -08:00
parent d4ba912955
commit 8a46d23268
13 changed files with 525 additions and 78 deletions

View File

@ -1,8 +1,15 @@
# node-triton changelog
## 2.1.5 (not yet released)
## 3.0.0 (not yet released)
(nothing yet)
- #54 RBAC support, see <https://docs.joyent.com/public-cloud/rbac> to start.
- [Backward incompatible.] The `triton` CLI option for the cloudapi URL has
changed from `--url,-u` to **`--url,-U`**.
- Add `triton --user,-u USER` CLI option and `TRITON_USER` (or `SDC_USER`)
environment variable support for specifying the RBAC user.
- `triton profiles` now shows the optional `user` fields.
- A (currently experimental and hidden) `triton rbac ...` command to
house RBAC CLI functionality.
## 2.1.4

View File

@ -84,14 +84,14 @@ var OPTIONS = [
'or SDC_ACCOUNT=ACCOUNT.',
helpArg: 'ACCOUNT'
},
// TODO: subuser/RBAC support
//{
// names: ['subuser', 'user'],
// type: 'string',
// env: 'MANTA_SUBUSER',
// help: 'Manta User (login name)',
// helpArg: 'USER'
//},
{
names: ['user', 'u'],
type: 'string',
help: 'RBAC user (login name). Environment: TRITON_USER=USER ' +
'or SDC_USER=USER.',
helpArg: 'USER'
},
// TODO: full rbac support
//{
// names: ['role'],
// type: 'arrayOfString',
@ -107,7 +107,7 @@ var OPTIONS = [
helpArg: 'FINGERPRINT'
},
{
names: ['url', 'u'],
names: ['url', 'U'],
type: 'string',
help: 'CloudAPI URL. Environment: TRITON_URL=URL or SDC_URL=URL.',
helpArg: 'URL'
@ -253,7 +253,7 @@ CLI.prototype.init = function (opts, args, callback) {
CLI.prototype._applyProfileOverrides =
function _applyProfileOverrides(profile) {
var self = this;
['account', 'url', 'keyId', 'insecure'].forEach(function (field) {
['account', 'user', 'url', 'keyId', 'insecure'].forEach(function (field) {
// We need to check `opts._order` to know if boolean opts
// were specified.
var specified = self.opts._order.filter(
@ -305,6 +305,7 @@ CLI.prototype.do_network = require('./do_network');
// Hidden commands
CLI.prototype.do_cloudapi = require('./do_cloudapi');
CLI.prototype.do_badger = require('./do_badger');
CLI.prototype.do_rbac = require('./do_rbac');

View File

@ -16,11 +16,15 @@
* var cloudapi = require('./lib/cloudapi2');
* var client = cloudapi.createClient({
* url: <URL>, // 'https://us-sw-1.api.joyent.com',
* user: <USER>, // 'bob'
* account: <ACCOUNT>, // 'acmecorp'
* [user: <RBAC-USER>,] // 'bob'
* log: <BUNYAN-LOGGER>,
* sign: auth.cliSigner({
* keyId: <KEY-ID>, // ssh fingerprint
* user: <USER>, // 'bob'
* // Unfortunately node-smartdc-auth uses user/subuser, while
* // node-triton uses account/user:
* user: <ACCOUNT>, // 'acmecorp'
* [subuser: <RBAC-USER>,] // 'bob'
* log: <BUNYAN-LOGGER>,
* }),
* ...
@ -72,9 +76,9 @@ BunyanNoopLogger.prototype.end = function () {};
*
* @param options {Object}
* - {String} url (required) Cloud API base url
* - {String} user (required) The user login name.
* For backward compat, 'options.account' is accepted as a synonym.
* - {String} account (required) The account login name.
* - {Function} sign (required) An http-signature auth signing function
* - {String} user (optional) The RBAC user login name.
* - {String} version (optional) Used for the accept-version header. This
* defaults to '*', meaning that over time you could experience breaking
* changes. Specifying a value is strongly recommended. E.g. '~7.1'.
@ -96,13 +100,15 @@ BunyanNoopLogger.prototype.end = function () {};
function CloudApi(options) {
assert.object(options, 'options');
assert.string(options.url, 'options.url');
assert.string(options.user || options.account, 'options.user');
assert.string(options.account, 'options.account');
assert.func(options.sign, 'options.sign');
assert.optionalString(options.user, 'options.user');
assert.optionalString(options.version, 'options.version');
assert.optionalObject(options.log, 'options.log');
this.url = options.url;
this.user = options.user || options.account;
this.account = options.account;
this.user = options.user; // optional RBAC subuser
this.sign = options.sign;
this.log = options.log || new BunyanNoopLogger();
if (!options.version) {
@ -118,7 +124,7 @@ function CloudApi(options) {
// return (code === 500);
//};
// XXX relevant?
// TODO support token auth
//this.token = options.token;
this.client = new SaferJsonClient(options);
@ -133,7 +139,7 @@ CloudApi.prototype._getAuthHeaders = function _getAuthHeaders(callback) {
headers.date = new Date().toUTCString();
var sigstr = 'date: ' + headers.date;
//XXX
// TODO: token auth support
//if (this.token !== undefined) {
// obj.headers['X-Auth-Token'] = this.token;
//}
@ -144,9 +150,13 @@ CloudApi.prototype._getAuthHeaders = function _getAuthHeaders(callback) {
return;
}
var ident = self.account;
if (self.user) {
ident += '/users/' + self.user;
}
headers.authorization = format(
'Signature keyId="/%s/keys/%s",algorithm="%s",signature="%s"',
self.user, sig.keyId, sig.algorithm, sig.signature);
ident, sig.keyId, sig.algorithm, sig.signature);
callback(null, headers);
});
};
@ -178,7 +188,7 @@ CloudApi.prototype._qs = function _qs(/* fields1, ...*/) {
/**
* Return an appropriate full URL *path* given an CloudApi subpath.
* Return an appropriate full URL *path* given a CloudApi subpath.
* This handles prepending the API's base path, if any: e.g. if the configured
* URL is "https://example.com/base/path".
*
@ -244,8 +254,7 @@ CloudApi.prototype._request = function _request(options, callback) {
* A simple wrapper around making a GET request to an endpoint and
* passing back the body returned
*/
CloudApi.prototype._passThrough =
function _passThrough(endpoint, opts, cb) {
CloudApi.prototype._passThrough = function _passThrough(endpoint, opts, cb) {
var self = this;
if (typeof (opts) === 'function') {
cb = opts;
@ -278,6 +287,8 @@ function _passThrough(endpoint, opts, cb) {
});
};
// ---- networks
/**
@ -286,7 +297,7 @@ function _passThrough(endpoint, opts, cb) {
* @param {Function} callback of the form `function (err, networks, response)`
*/
CloudApi.prototype.listNetworks = function listNetworks(opts, cb) {
var endpoint = format('/%s/networks', this.user);
var endpoint = format('/%s/networks', this.account);
this._passThrough(endpoint, opts, cb);
};
@ -300,12 +311,14 @@ CloudApi.prototype.getNetwork = function getNetwork(id, cb) {
assert.uuid(id, 'id');
assert.func(cb, 'cb');
var endpoint = this._path(format('/%s/networks/%s', this.user, id));
var endpoint = this._path(format('/%s/networks/%s', this.account, id));
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
// ---- datacenters
/**
@ -314,7 +327,7 @@ CloudApi.prototype.getNetwork = function getNetwork(id, cb) {
* @param {Function} callback of the form `function (err, services, response)`
*/
CloudApi.prototype.listServices = function listServices(opts, cb) {
var endpoint = format('/%s/services', this.user);
var endpoint = format('/%s/services', this.account);
this._passThrough(endpoint, opts, cb);
};
@ -325,10 +338,11 @@ CloudApi.prototype.listServices = function listServices(opts, cb) {
* `function (err, datacenters, response)`
*/
CloudApi.prototype.listDatacenters = function listDatacenters(opts, cb) {
var endpoint = format('/%s/datacenters', this.user);
var endpoint = format('/%s/datacenters', this.account);
this._passThrough(endpoint, opts, cb);
};
// ---- accounts
/**
@ -337,7 +351,7 @@ CloudApi.prototype.listDatacenters = function listDatacenters(opts, cb) {
* @param {Function} callback of the form `function (err, account, response)`
*/
CloudApi.prototype.getAccount = function getAccount(opts, cb) {
var endpoint = format('/%s', this.user);
var endpoint = format('/%s', this.account);
this._passThrough(endpoint, opts, cb);
};
@ -347,68 +361,76 @@ CloudApi.prototype.getAccount = function getAccount(opts, cb) {
* @param {Function} callback of the form `function (err, keys, response)`
*/
CloudApi.prototype.listKeys = function listKeys(opts, cb) {
var endpoint = format('/%s/keys', this.user);
var endpoint = format('/%s/keys', this.account);
this._passThrough(endpoint, opts, cb);
};
// ---- images
/**
* <http://apidocs.joyent.com/cloudapi/#ListImages>
*
* @param {Object} options (optional)
* @param {Object} opts (optional)
* XXX be more strict about accepted options
* XXX document this, see the api doc above :)
* @param {Function} callback of the form `function (err, images, res)`
* @param {Function} cb of the form `function (err, images, res)`
*/
CloudApi.prototype.listImages = function listImages(opts, cb) {
var endpoint = format('/%s/images', this.user);
var endpoint = format('/%s/images', this.account);
this._passThrough(endpoint, opts, cb);
};
/**
* <http://apidocs.joyent.com/cloudapi/#ListImages>
* <http://apidocs.joyent.com/cloudapi/#GetImage>
*
* @param {Object} options
* @param {Object} opts
* - id {UUID}
* @param {Function} callback of the form `function (err, image, res)`
* @param {Function} cb of the form `function (err, image, res)`
*/
CloudApi.prototype.getImage = function getImage(options, callback) {
if (callback === undefined) {
callback = options;
options = {};
}
assert.object(options, 'options');
assert.uuid(options.id, 'options.id');
assert.func(callback, 'callback');
CloudApi.prototype.getImage = function getImage(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.func(cb, 'cb');
var endpoint = this._path(format('/%s/images/%s', this.user, options.id));
var endpoint = this._path(format('/%s/images/%s', this.account, opts.id));
this._request(endpoint, function (err, req, res, body) {
callback(err, body, res);
cb(err, body, res);
});
};
// ---- packages
/**
* <http://apidocs.joyent.com/cloudapi/#ListPackages>
*
* @param {Object} opts (optional)
* XXX be more strict about accepted options
* XXX document this, see the api doc above :)
* @param {Function} callback of the form `function (err, packages, res)`
*/
CloudApi.prototype.listPackages = function listPackages(opts, cb) {
var endpoint = format('/%s/packages', this.user);
var endpoint = format('/%s/packages', this.account);
this._passThrough(endpoint, opts, cb);
};
CloudApi.prototype.getPackage = function getPackage(options, callback) {
if (callback === undefined) {
callback = options;
options = {};
}
assert.object(options, 'options');
assert.uuid(options.id, 'options.id');
assert.func(callback, 'callback');
/**
* <http://apidocs.joyent.com/cloudapi/#GetPackage>
*
* @param {Object} opts
* - id {UUID|String} Package ID (a UUID) or name.
* @param {Function} cb of the form `function (err, package, res)`
*/
CloudApi.prototype.getPackage = function getPackage(opts, cb) {
assert.object(opts, 'opts');
assert.uuid(opts.id, 'opts.id');
assert.func(cb, 'cb');
var endpoint = this._path(format('/%s/packages/%s', this.user, options.id));
// XXX use _passThrough?
var endpoint = this._path(format('/%s/packages/%s', this.account, opts.id));
this._request(endpoint, function (err, req, res, body) {
callback(err, body, res);
cb(err, body, res);
});
};
@ -428,7 +450,7 @@ CloudApi.prototype.getMachine = function getMachine(id, cb) {
assert.uuid(id, 'id');
assert.func(cb, 'cb');
var endpoint = format('/%s/machines/%s', this.user, id);
var endpoint = format('/%s/machines/%s', this.account, id);
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
@ -446,7 +468,7 @@ CloudApi.prototype.deleteMachine = function deleteMachine(uuid, callback) {
assert.func(callback, 'callback');
var opts = {
path: format('/%s/machines/%s', self.user, uuid),
path: format('/%s/machines/%s', self.account, uuid),
method: 'DELETE'
};
this._request(opts, function (err, req, res, body) {
@ -494,7 +516,7 @@ CloudApi.prototype._doMachine = function _doMachine(action, uuid, callback) {
assert.func(callback, 'callback');
var opts = {
path: format('/%s/machines/%s', self.user, uuid),
path: format('/%s/machines/%s', self.account, uuid),
method: 'POST',
data: {
action: action
@ -542,7 +564,7 @@ CloudApi.prototype.waitForMachineStates =
};
/**
* List the user's machines.
* List the account's machines.
* <http://apidocs.joyent.com/cloudapi/#ListMachines>
*
* @param {Object} options
@ -554,7 +576,7 @@ function createListMachinesStream(options) {
var self = this;
options = options || {};
// if the user specifies an offset we don't paginate
// If a `limit` is specified, we don't paginate.
var once = options.limit !== undefined;
return new LOMStream({
@ -566,7 +588,8 @@ function createListMachinesStream(options) {
function fetch(fetcharg, limitObj, datacb, donecb) {
options.limit = limitObj.limit;
options.offset = limitObj.offset;
var endpoint = self._path(format('/%s/machines', self.user), options);
var endpoint = self._path(
format('/%s/machines', self.account), options);
self._request(endpoint, function (err, req, res, body) {
if (err) {
@ -580,7 +603,7 @@ function createListMachinesStream(options) {
};
/**
* List the user's machines.
* List the account's machines.
* <http://apidocs.joyent.com/cloudapi/#ListMachines>
*
* @param {Object} options
@ -621,7 +644,7 @@ CloudApi.prototype.createMachine = function createMachine(options, callback) {
// XXX how does options.networks array work here?
this._request({
method: 'POST',
path: format('/%s/machines', this.user),
path: format('/%s/machines', this.account),
data: options
}, function (err, req, res, body) {
callback(err, body, res);
@ -641,13 +664,54 @@ CloudApi.prototype.machineAudit = function machineAudit(id, cb) {
assert.uuid(id, 'id');
assert.func(cb, 'cb');
var endpoint = format('/%s/machines/%s/audit', this.user, id);
var endpoint = format('/%s/machines/%s/audit', this.account, id);
this._request(endpoint, function (err, req, res, body) {
cb(err, body, res);
});
};
// --- rbac
/**
* <http://apidocs.joyent.com/cloudapi/#ListUsers>
*
* @param opts {Object} Options (optional)
* @param cb {Function} Callback of the form `function (err, users, res)`
*/
CloudApi.prototype.listUsers = function listUsers(opts, cb) {
if (cb === undefined) {
cb = opts;
opts = {};
}
assert.func(cb, 'cb');
assert.object(opts, 'opts');
var endpoint = format('/%s/users', this.account);
this._passThrough(endpoint, opts, cb);
};
/**
* <http://apidocs.joyent.com/cloudapi/#GetUser>
*
* @param {Object} opts
* - id {UUID|String} The user ID or login.
* - membership {Boolean} Optional. Whether to includes roles of which
* this user is a member. Default false.
* @param {Function} callback of the form `function (err, user, res)`
*/
CloudApi.prototype.getUser = function getUser(opts, cb) {
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.optionalBool(opts.membership, 'opts.membership');
assert.func(cb, 'cb');
var endpoint = format('/%s/users/%s', this.account, opts.id);
this._passThrough(endpoint, {membership: opts.membership}, cb);
};
// --- Exports
module.exports.createClient = function (options) {

View File

@ -318,7 +318,7 @@ function normShortId(s) {
return;
}
shortId += head;
if (remaining) {
if (remaining && i + 1 < spans.length) {
shortId += '-';
} else {
break;

View File

@ -58,7 +58,8 @@ var PROFILE_FIELDS = {
url: true,
account: true,
keyId: true,
insecure: true
insecure: true,
user: true
};
@ -190,6 +191,7 @@ function validateProfile(profile, profilePath) {
assert.string(profile.account, 'profile.account');
assert.string(profile.keyId, 'profile.keyId');
assert.optionalBool(profile.insecure, 'profile.insecure');
assert.optionalString(profile.user, 'profile.user');
} catch (err) {
throw new errors.ConfigError(err.message);
}
@ -224,6 +226,7 @@ function _loadEnvProfile() {
};
envProfile.account = process.env.TRITON_ACCOUNT || process.env.SDC_ACCOUNT;
envProfile.user = process.env.TRITON_USER || process.env.SDC_USER;
envProfile.url = process.env.TRITON_URL || process.env.SDC_URL;
envProfile.keyId = process.env.TRITON_KEY_ID || process.env.SDC_KEY_ID;
if (process.env.TRITON_TLS_INSECURE) {

View File

@ -7,7 +7,7 @@
/*
* Copyright 2015 Joyent, Inc.
*
* `triton account ...`
* `triton keys ...`
*/
var common = require('./common');

View File

@ -44,9 +44,11 @@ function _showProfile(opts, cb) {
if (opts.json) {
console.log(JSON.stringify(profile));
} else {
console.log('name: %s', profile.name);
Object.keys(profile).sort().forEach(function (key) {
var val = profile[key];
console.log('%s: %s', key, val);
if (key === 'name')
return;
console.log('%s: %s', key, profile[key]);
});
}
}

View File

@ -11,8 +11,8 @@ var tabula = require('tabula');
var sortDefault = 'name';
var columnsDefault = 'name,curr,account,url';
var columnsDefaultLong = 'name,curr,account,url,insecure,keyId';
var columnsDefault = 'name,curr,account,user,url';
var columnsDefaultLong = 'name,curr,account,user,url,insecure,keyId';
function _listProfiles(cli, opts, args, cb) {
var columns = columnsDefault;

81
lib/do_rbac/do_user.js Normal file
View File

@ -0,0 +1,81 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2015 Joyent, Inc.
*
* `triton rbac user ...`
*/
var errors = require('../errors');
function do_user(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 1) {
return cb(new errors.UsageError('incorrect number of args'));
}
this.top.tritonapi.getUser({
id: args[0],
roles: opts.roles || opts.membership
}, function onUser(err, user) {
if (err) {
return cb(err);
}
if (opts.json) {
console.log(JSON.stringify(user));
} else {
console.log(JSON.stringify(user, null, 4));
}
cb();
});
}
do_user.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
},
{
names: ['json', 'j'],
type: 'bool',
help: 'JSON stream output.'
},
{
names: ['roles', 'r'],
type: 'bool',
help: 'Include "roles" and "default_roles" this user has.'
},
{
names: ['membership'],
type: 'bool',
help: 'Include "roles" and "default_roles" this user has. Included ' +
'for backward compat with `sdc-user get --membership ...` from ' +
'node-smartdc.',
hidden: true
}
];
do_user.help = (
/* BEGIN JSSTYLED */
'Get an RBAC user.\n' +
'\n' +
'Usage:\n' +
' {{name}} user [<options>] ID|LOGIN|SHORT-ID\n' +
'\n' +
'{{options}}' +
'\n' +
'Note: Currently this dumps indented JSON by default. That might change\n' +
'in the future. Use "-j" to explicitly get JSON output.\n'
/* END JSSTYLED */
);
module.exports = do_user;

112
lib/do_rbac/do_users.js Normal file
View File

@ -0,0 +1,112 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2015 Joyent, Inc.
*
* `triton rbac users ...`
*/
var tabula = require('tabula');
var common = require('../common');
var errors = require('../errors');
// columns default without -o
var columnsDefault = 'shortid,login,email,name,cdate';
// columns default with -l
var columnsDefaultLong = 'id,login,email,firstName,lastName,created';
// sort default with -s
var sortDefault = 'login';
function do_users(subcmd, opts, args, cb) {
if (opts.help) {
this.do_help('help', {}, [subcmd], cb);
return;
} else if (args.length !== 0) {
cb(new errors.UsageError('invalid args: ' + args));
return;
}
var columns = columnsDefault;
if (opts.o) {
columns = opts.o;
} else if (opts.long) {
columns = columnsDefaultLong;
}
columns = columns.split(',');
var sort = opts.s.split(',');
this.top.tritonapi.cloudapi.listUsers(function (err, users) {
if (err) {
cb(err);
return;
}
if (opts.json) {
common.jsonStream(users);
} else {
// Add some convenience fields
for (var i = 0; i < users.length; i++) {
var user = users[i];
user.shortid = user.id.split('-', 1)[0];
user.name = ((user.firstName || '') + ' ' +
(user.lastName || '')).trim() || undefined;
if (user.created) {
user.cdate = user.created.slice(0, 10); // Just the date.
}
if (user.updated) {
user.udate = user.updated.slice(0, 10); // Just the date.
}
}
tabula(users, {
skipHeader: opts.H,
columns: columns,
sort: sort
});
}
cb();
});
}
do_users.options = [
{
names: ['help', 'h'],
type: 'bool',
help: 'Show this help.'
}
].concat(common.getCliTableOptions({
includeLong: true,
sortDefault: sortDefault
}));
do_users.help = (
/* BEGIN JSSTYLED */
'List RBAC users.\n' +
'\n' +
'Usage:\n' +
' {{name}} users [<options>]\n' +
'\n' +
'Fields (most are self explanatory, the client adds some for convenience):\n' +
' shortid A short ID prefix.\n' +
' name "firstName lastName"\n' +
' cdate Just the date portion of "created"\n' +
' udate Just the date portion of "updated"\n' +
'\n' +
'{{options}}'
/* END JSSTYLED */
);
module.exports = do_users;

47
lib/do_rbac/index.js Normal file
View File

@ -0,0 +1,47 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright 2015 Joyent, Inc.
*
* `triton rbac ...`
*/
var Cmdln = require('cmdln').Cmdln;
var util = require('util');
// ---- CLI class
function RbacCLI(top) {
this.top = top;
Cmdln.call(this, {
name: top.name + ' rbac',
/* BEGIN JSSTYLED */
desc: 'Role-based Access Control (RBAC) commands. *Experimental.*\n' +
'\n' +
'See <https://docs.joyent.com/public-cloud/rbac> for a general start.',
/* END JSSTYLED */
helpOpts: {
minHelpCol: 24 /* line up with option help */
}
});
}
util.inherits(RbacCLI, Cmdln);
RbacCLI.hidden = true;
RbacCLI.prototype.init = function init(opts, args, cb) {
this.log = this.top.log;
Cmdln.prototype.init.apply(this, arguments);
};
RbacCLI.prototype.do_users = require('./do_users');
RbacCLI.prototype.do_user = require('./do_user');
module.exports = RbacCLI;

View File

@ -10,7 +10,6 @@
* Core TritonApi client driver class.
*/
var p = console.log;
var assert = require('assert-plus');
var auth = require('smartdc-auth');
var EventEmitter = require('events').EventEmitter;
@ -91,6 +90,7 @@ TritonApi.prototype._cloudapiFromProfile =
assert.string(profile.account, 'profile.account');
assert.string(profile.keyId, 'profile.keyId');
assert.string(profile.url, 'profile.url');
assert.optionalString(profile.user, 'profile.user');
assert.optionalString(profile.privKey, 'profile.privKey');
assert.optionalBool(profile.insecure, 'profile.insecure');
var rejectUnauthorized = (profile.insecure === undefined
@ -100,18 +100,21 @@ TritonApi.prototype._cloudapiFromProfile =
if (profile.privKey) {
sign = auth.privateKeySigner({
user: profile.account,
subuser: profile.user,
keyId: profile.keyId,
key: profile.privKey
});
} else {
sign = auth.cliSigner({
keyId: profile.keyId,
user: profile.account
user: profile.account,
subuser: profile.user
});
}
var client = cloudapi.createClient({
url: profile.url,
user: profile.account,
account: profile.account,
user: profile.user,
version: '*',
rejectUnauthorized: rejectUnauthorized,
sign: sign,
@ -585,6 +588,133 @@ TritonApi.prototype.getInstance = function getInstance(name, cb) {
};
/**
* Get an RBAC user by ID, login, or short ID, in that order.
*
* @param {Object} opts
* - id {UUID|String} The user ID (a UUID), login or short id.
* - roles {Boolean} Optional. Whether to includes roles of which this
* user is a member. Default false.
* @param {Function} callback of the form `function (err, user)`
*/
TritonApi.prototype.getUser = function getUser(opts, cb) {
var self = this;
assert.object(opts, 'opts');
assert.string(opts.id, 'opts.id');
assert.optionalBool(opts.roles, 'opts.roles');
assert.func(cb, 'cb');
/*
* CloudAPI GetUser supports a UUID or login, so we try that first.
* If that is a 404 and `opts.id` a valid shortid, then try to lookup
* via `listUsers`.
*/
var context = {};
vasync.pipeline({arg: context, funcs: [
function tryGetUser(ctx, next) {
var getOpts = {
id: opts.id,
membership: opts.roles
};
self.cloudapi.getUser(getOpts, function (err, user) {
if (err) {
if (err.restCode === 'ResourceNotFound') {
ctx.notFoundErr = err;
next();
} else {
next(err);
}
} else {
ctx.user = user;
next();
}
});
},
function tryShortId(ctx, next) {
if (ctx.user) {
next();
return;
}
var shortId = common.normShortId(opts.id);
if (!shortId) {
next();
return;
}
self.cloudapi.listUsers(function (err, users) {
if (err) {
next(err);
return;
}
var shortIdMatches = [];
for (var i = 0; i < users.length; i++) {
var user = users[i];
// TODO: use this test in other shortId matching
if (user.id.slice(0, shortId.length) === shortId) {
shortIdMatches.push(user);
}
}
if (shortIdMatches.length === 1) {
ctx.user = shortIdMatches[0];
next();
} else if (shortIdMatches.length === 0) {
next(new errors.ResourceNotFoundError(format(
'user with login or id matching "%s" was not found',
opts.id)));
} else {
next(new errors.ResourceNotFoundError(
format('user with login "%s" was not found '
+ 'and "%s" is an ambiguous short id', opts.id)));
}
});
},
/*
* If we found the user via `listUsers` and `opts.roles` was requested
* then we need to re-getUser.
*/
function reGetUserIfNecessary(ctx, next) {
if (!ctx.user) {
// We must have gotten the `notFoundErr` above.
next(new errors.ResourceNotFoundError(ctx.notFoundErr, format(
'user with login or id %s was not found', opts.id)));
return;
} else if (!opts.roles || ctx.user.roles) {
next();
return;
}
var getOpts = {
id: ctx.user.id,
membership: opts.roles
};
self.cloudapi.getUser(getOpts, function (err, user) {
if (err) {
if (err.restCode === 'ResourceNotFound') {
next(new errors.ResourceNotFoundError(err, format(
'user with id %s was not found', opts.id)));
} else {
next(err);
}
} else {
ctx.user = user;
next();
}
});
}
]}, function (err) {
cb(err, context.user);
});
};
//---- exports
module.exports.createClient = function (options) {

View File

@ -1,7 +1,7 @@
{
"name": "triton",
"description": "Joyent Triton CLI and client (https://www.joyent.com/triton)",
"version": "2.1.5",
"version": "3.0.0",
"author": "Joyent (joyent.com)",
"dependencies": {
"assert-plus": "0.1.5",