@yawetse/pkgcloud
Version:
An infrastructure-as-a-service agnostic cloud library for node.js
397 lines (357 loc) • 12.3 kB
JavaScript
/*
* identity.js: Identity for openstack authentication
*
* (C) 2013 Rackspace, Ken Perkins
* (C) 2015 IBM Corp.
* MIT LICENSE
*
*/
var _ = require('underscore'),
events = require('eventemitter2'),
request = require('request'),
ServiceCatalog = require('./serviceCatalog').ServiceCatalog,
urlJoin = require('url-join'),
util = require('util'),
pkgcloud = require('../../../pkgcloud');
// TODO refactor failCodes, getError into global handlers
var failCodes = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Resize not allowed',
404: 'Item not found',
405: 'Bad Method',
409: 'Build in progress',
413: 'Over Limit',
415: 'Bad Media Type',
500: 'Fault',
503: 'Service Unavailable'
};
function getError(err, res, body) {
if (err) {
return err;
}
var statusCode = res.statusCode.toString(),
err2;
if (Object.keys(failCodes).indexOf(statusCode) !== -1) {
//
// TODO: Support more than JSON errors here
//
err2 = {
failCode: failCodes[statusCode],
statusCode: res.statusCode,
message: 'Error (' +
statusCode + '): ' + failCodes[statusCode],
href: res.request.uri.href,
method: res.request.method,
headers: res.headers
};
try {
err2.result = typeof body === 'string' ? JSON.parse(body) : body;
} catch (e) {
err2.result = { err: body };
}
return err2;
}
}
/**
* Identity object
*
* @description Base Identity object for Openstack Keystone
*
* @param options
* @constructor
*/
var Identity = exports.Identity = function (options) {
var self = this;
events.EventEmitter2.call(this, { delimiter: '::', wildcard: true });
self.options = options || {};
self.name = 'OpenstackIdentity';
self.basePath = options.basePath || (options.keystoneAuthVersion === 'v3' ? '/v3/auth/tokens' : '/v2.0/tokens');
self.useServiceCatalog = (typeof options.useServiceCatalog === 'boolean')
? options.useServiceCatalog
: true;
_.each(['url'], function (value) {
if (!self.options[value]) {
throw new Error('options.' + value + ' is a required option');
}
});
};
util.inherits(Identity, events.EventEmitter2);
/**
* Identity.authorize
*
* @description this function is the guts of authorizing against an openstack
* identity endpoint.
* @param {object} options the options for authorization
* @param callback
*/
Identity.prototype.authorize = function (options, callback) {
var self = this;
if (typeof options === 'function') {
callback = options;
options = {};
}
var authenticationOptions = {
uri: urlJoin(options.url || self.options.url, self.basePath),
method: 'POST',
strictSSL: options.strictSSL || self.options.strictSSL,
headers: {
'User-Agent': util.format('nodejs-pkgcloud/%s', pkgcloud.version),
'Content-Type': 'application/json',
'Accept': 'application/json'
}
};
if (self.options.headers) {
for (var header in self.options.headers) {
if (self.options.headers.hasOwnProperty(header)) {
authenticationOptions.headers[header] = self.options.headers[header];
}
}
}
if (self.options.version === 1 || self.options.version === '/v1.0') {
authenticationOptions.uri = urlJoin(options.url || self.options.url, '/auth/v1.0');
authenticationOptions.method = 'GET';
authenticationOptions.headers['X-Auth-User'] = self.options.username;
authenticationOptions.headers['X-Auth-Key'] = self.options.password;
}
self._buildAuthenticationPayload();
// we can't be called without a payload
if (!self._authenticationPayload) {
return process.nextTick(function () {
callback(new Error('Unable to authorize; missing required inputs'));
});
}
// Are we filtering down by a tenant?
if (self.options.tenantId) {
self._authenticationPayload.auth.tenantId = self.options.tenantId;
}
else if (self.options.tenantName) {
self._authenticationPayload.auth.tenantName = self.options.tenantName;
}
authenticationOptions.json = self._authenticationPayload;
self.emit('log::trace', 'Sending client authorization request', authenticationOptions);
// Don't keep a copy of the credentials in memory
delete self._authenticationPayload;
request(authenticationOptions, function (err, response, body) {
// check for a network error, or a handled error
var err2 = getError(err, response, body);
if (err2) {
return callback(err2);
}
self.emit('log::trace', 'Provider Authentication Response', {
href: response.request.uri.href,
method: response.request.method,
headers: response.headers,
statusCode: response.statusCode
});
// If we've been asked to do v1 auth, check the response headers
// otherwise, check the body
try {
if (self.options.version === 1 || self.options.version === '/v1.0') {
self._storageUrl = response.headers['x-storage-url'];
self.token = {
id: response.headers['x-auth-token']
};
callback();
}
// If we don't have a tenantId in the response (meaning no service catalog)
// go ahead and make a 1-off request to get a tenant and then reauthorize
else if (self.options.keystoneAuthVersion !== 'v3' && !body.access.token.tenant) {
getTenantId(urlJoin(options.url || self.options.url, '/v2.0/tenants'), body.access.token.id);
}
else {
self._parseIdentityResponse(body, response.headers);
callback();
}
}
catch (e) {
callback(e);
}
});
function getTenantId(endpoint, token) {
var tenantOptions = {
uri: endpoint,
json: true,
strictSSL: options.strictSSL || self.options.strictSSL,
headers: {
'X-Auth-Token': token,
'Content-Type': 'application/json',
'User-Agent': util.format('nodejs-pkgcloud/%s', pkgcloud.version)
}
};
request(tenantOptions, function (err, response, body) {
if (err || !body.tenants || !body.tenants.length) {
return callback(err ? err : new Error('Unable to find tenants'));
}
var firstActiveTenant;
body.tenants.forEach(function (tenant) {
if (!firstActiveTenant && !!tenant.enabled && tenant.enabled !== 'false') {
firstActiveTenant = tenant;
}
});
if (!firstActiveTenant) {
return callback(new Error('Unable to find an active tenant'));
}
// TODO make this more resiliant (what if multiple active tenants)
self.options.tenantId = firstActiveTenant.id;
self.authorize(options, callback);
});
}
};
/**
* Identity._buildAuthenticationPayload
*
* @description processes the authentication options into a valid payload for
* authorization
*
* @private
*/
Identity.prototype._buildAuthenticationPayload = function () {
var self = this;
self.emit('log::trace', 'Building Openstack Identity Auth Payload');
if (self.options.keystoneAuthVersion === 'v3') {
if (self.options.password) {
self._authenticationPayload = {
auth: {
identity : {
methods : ['password'],
password : {
user: {
password: self.options.password
}
}
}
}
};
//first add user name or id to user field
if (self.options.username) {
self._authenticationPayload.auth.identity.password.user.name = self.options.username;
} else if (self.options.userid) {
self._authenticationPayload.auth.identity.password.user.id = self.options.userid;
}
//check if authenticating against user domain
if (self.options.domainId) {
self._authenticationPayload.auth.identity.password.user.domain = {id:self.options.domainId};
} else if (self.options.domainName) {
self._authenticationPayload.auth.identity.password.user.domain = {name:self.options.domainName};
}
//check if we're getting a scoped token against a project and/or domain
if (self.options.tenantId || self.options.tenantName || self.options.projectDomainName || self.options.projectDomainId) {
self._authenticationPayload.auth.scope = {};
var scopedProject = true;
if (self.options.tenantId) {
self._authenticationPayload.auth.scope.project = {id:self.options.tenantId};
} else if (self.options.tenantName) {
self._authenticationPayload.auth.scope.project = {name:self.options.tenantName};
} else {
scopedProject = false;
}
if (!scopedProject) {
if (self.options.projectDomainId) {
self._authenticationPayload.auth.scope.domain = {id:self.options.projectDomainId};
} else if (self.options.projectDomainName) {
self._authenticationPayload.auth.scope.domain = {name:self.options.projectDomainName};
}
} else {
if (self.options.projectDomainId) {
self._authenticationPayload.auth.scope.project.domain = {id:self.options.projectDomainId};
} else if(self.options.projectDomainName) {
self._authenticationPayload.auth.scope.project.domain = {name:self.options.projectDomainName};
}
}
}
}
// Token and tenant are also valid inputs
else if (self.options.token && (self.options.tenantId || self.options.tenantName)) {
self._authenticationPayload = {
auth: {
identity : {
methods : ['token'],
token: {
id: self.options.token
}
}
}
};
}
} else {
// setup our inputs for authorization
if (self.options.password && self.options.username) {
self._authenticationPayload = {
auth: {
passwordCredentials: {
username: self.options.username,
password: self.options.password
}
}
};
}
// Token and tenant are also valid inputs
else if (self.options.token && (self.options.tenantId || self.options.tenantName)) {
self._authenticationPayload = {
auth: {
token: {
id: self.options.token
}
}
};
}
// Are we filtering down by a tenant?
if (self._authenticationPayload && self.options.tenantId) {
self._authenticationPayload.auth.tenantId = self.options.tenantId;
}
else if (self._authenticationPayload && self.options.tenantName) {
self._authenticationPayload.auth.tenantName = self.options.tenantName;
}
}
};
/**
* Identity._parseIdentityResponse
*
* @description takes the full identity response and deserializes it into a
* serviceCatalog object with services.
*
* @param {object} data the raw response from the identity call
* @private
*/
Identity.prototype._parseIdentityResponse = function (data, headers) {
var self = this;
if (!data) {
throw new Error('missing required arguments!');
}
if (self.options.keystoneAuthVersion === 'v3') {
self.token = {
id: headers['x-subject-token'],
expires: new Date(data.token.expires_at),
issued_at: new Date(data.token.issued_at),
tenant: {id: data.token.project.id, name: data.token.project.name}
};
self.user = data.token.user;
if (self.useServiceCatalog) {
self.serviceCatalog = new ServiceCatalog(data.token.catalog);
}
} else {
if (data.access.token) {
self.token = data.access.token;
self.token.expires = new Date(self.token.expires);
}
if (self.useServiceCatalog && data.access.serviceCatalog) {
self.serviceCatalog = new ServiceCatalog(data.access.serviceCatalog);
}
self.user = data.access.user;
}
self.raw = data;
};
Identity.prototype.getServiceEndpointUrl = function (options) {
if (this.useServiceCatalog) {
return this.serviceCatalog.getServiceEndpointUrl(options);
}
// This is a hack to enable support for v1 swift clients
// TODO move all auth for v1 swift out of identity, as it's not related to identity at all
else if (this.options.version === 1 || this.options.version === '/v1.0') {
return this._storageUrl;
}
else {
return this.options.url;
}
};