joyent/node-triton#54 A start at RBAC support (still very early)
This commit is contained in:
parent
d4ba912955
commit
8a46d23268
11
CHANGES.md
11
CHANGES.md
@ -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
|
||||
|
21
lib/cli.js
21
lib/cli.js
@ -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');
|
||||
|
||||
|
||||
|
||||
|
174
lib/cloudapi2.js
174
lib/cloudapi2.js
@ -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) {
|
||||
|
@ -318,7 +318,7 @@ function normShortId(s) {
|
||||
return;
|
||||
}
|
||||
shortId += head;
|
||||
if (remaining) {
|
||||
if (remaining && i + 1 < spans.length) {
|
||||
shortId += '-';
|
||||
} else {
|
||||
break;
|
||||
|
@ -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) {
|
||||
|
@ -7,7 +7,7 @@
|
||||
/*
|
||||
* Copyright 2015 Joyent, Inc.
|
||||
*
|
||||
* `triton account ...`
|
||||
* `triton keys ...`
|
||||
*/
|
||||
|
||||
var common = require('./common');
|
||||
|
@ -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]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
81
lib/do_rbac/do_user.js
Normal 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
112
lib/do_rbac/do_users.js
Normal 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
47
lib/do_rbac/index.js
Normal 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;
|
136
lib/tritonapi.js
136
lib/tritonapi.js
@ -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) {
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user