Skip to content

Commit

Permalink
Adds AWS.RDS.Signer class
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisradek committed Apr 25, 2017
1 parent 0a54ed2 commit 35a8f57
Show file tree
Hide file tree
Showing 10 changed files with 491 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changes/next-release/feature-RDS-99a01e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "feature",
"category": "RDS",
"description": "Adds AWS.RDS.Signer class to generate auth tokens for connecting to a database."
}
12 changes: 11 additions & 1 deletion lib/event_listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ AWS.EventListeners = {
});

add('BUILD_IDEMPOTENCY_TOKENS', 'validate', function BUILD_IDEMPOTENCY_TOKENS(req) {
if (!req.service.api.operations) {
return;
}
var operation = req.service.api.operations[req.operation];
if (!operation) {
return;
Expand All @@ -103,13 +106,19 @@ AWS.EventListeners = {
});

add('VALIDATE_PARAMETERS', 'validate', function VALIDATE_PARAMETERS(req) {
if (!req.service.api.operations) {
return;
}
var rules = req.service.api.operations[req.operation].input;
var validation = req.service.config.paramValidation;
new AWS.ParamValidator(validation).validate(rules, req.params);
});

addAsync('COMPUTE_SHA256', 'afterBuild', function COMPUTE_SHA256(req, done) {
req.haltHandlersOnError();
if (!req.service.api.operations) {
return;
}
var operation = req.service.api.operations[req.operation];
var authtype = operation ? operation.authtype : '';
if (!req.service.api.signatureVersion && !authtype) return done(); // none
Expand Down Expand Up @@ -162,7 +171,8 @@ AWS.EventListeners = {

addAsync('SIGN', 'sign', function SIGN(req, done) {
var service = req.service;
var operation = req.service.api.operations[req.operation];
var operations = req.service.api.operations || {};
var operation = operations[req.operation];
var authtype = operation ? operation.authtype : '';
if (!service.api.signatureVersion && !authtype) return done(); // none

Expand Down
26 changes: 26 additions & 0 deletions lib/rds/signer.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Credentials, CredentialsOptions} from '../credentials';
import {AWSError} from '../error';
export class Signer {
/**
* A signer object can be used to generate an auth token to a database.
*/
constructor(options?:Signer.SignerOptions);
/**
* Generate an auth token to a database.
*/
getAuthToken(options: Signer.SignerOptions, callback: (err: AWSError, token: string) => void): void;
/**
* Generate an auth token to a database.
*/
getAuthToken(options: Signer.SignerOptions): string;
}

declare namespace Signer {
export interface SignerOptions {
credentials?: Credentials | CredentialsOptions;
region?: string;
hostname?: string;
port?: number;
username?: string;
}
}
214 changes: 214 additions & 0 deletions lib/rds/signer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
var AWS = require('../core');

/**
* @api private
*/
var service = null;

/**
* @api private
*/
var api = {
signatureVersion: 'v4',
signingName: 'rds-db'
};

/**
* @api private
*/
var requiredAuthTokenOptions = {
region: 'string',
hostname: 'string',
port: 'number',
username: 'string'
};

/**
* A signer object can be used to generate an auth token to a database.
*/
AWS.RDS.Signer = AWS.util.inherit({
/**
* Creates a signer object can be used to generate an auth token.
*
* @option options credentials [AWS.Credentials] the AWS credentials
* to sign requests with. Uses the default credential provider chain
* if not specified.
* @option options hostname [String] the hostname of the database to connect to.
* @option options port [Number] the port number the database is listening on.
* @option options region [String] the region the database is located in.
* @option options username [String] the username to login as.
* @example Passing in options to constructor
* var signer = new AWS.RDS.Signer({
* credentials: new AWS.SharedIniFileCredentials({profile: 'default'}),
* region: 'us-east-1',
* hostname: 'db.us-east-1.rds.amazonaws.com',
* port: 8000,
* username: 'name'
* });
*/
constructor: function Signer(options) {
this.options = options || {};
},

/**
* @api private
* Strips the protocol from a url.
*/
convertUrlToAuthToken: function convertUrlToAuthToken(url) {
// we are always using https as the protocol
var protocol = 'https://';
if (url.indexOf(protocol) === 0) {
return url.substring(protocol.length);
}
},

/**
* @overload getAuthToken(options = {}, [callback])
* Generate an auth token to a database.
* @note You must ensure that you have static or previously resolved
* credentials if you call this method synchronously (with no callback),
* otherwise it may not properly sign the request. If you cannot guarantee
* this (you are using an asynchronous credential provider, i.e., EC2
* IAM roles), you should always call this method with an asynchronous
* callback.
*
* @param options [map] The fields to use when generating an auth token.
* Any options specified here will be merged on top of any options passed
* to AWS.RDS.Signer:
*
* * **credentials** (AWS.Credentials) — the AWS credentials
* to sign requests with. Uses the default credential provider chain
* if not specified.
* * **hostname** (String) — the hostname of the database to connect to.
* * **port** (Number) — the port number the database is listening on.
* * **region** (String) — the region the database is located in.
* * **username** (String) — the username to login as.
* @return [String] if called synchronously (with no callback), returns the
* auth token.
* @return [null] nothing is returned if a callback is provided.
* @callback callback function (err, token)
* If a callback is supplied, it is called when an auth token has been generated.
* @param err [Error] the error object returned from the signer.
* @param token [String] the auth token.
*
* @example Generating an auth token synchronously
* var signer = new AWS.RDS.Signer({
* // configure options
* region: 'us-east-1',
* username: 'default',
* hostname: 'db.us-east-1.amazonaws.com',
* port: 8000
* });
* var token = signer.getAuthToken({
* // these options are merged with those defined when creating the signer, overriding in the case of a duplicate option
* // credentials are not specified here or when creating the signer, so default credential provider will be used
* username: 'test' // overriding username
* });
* @example Generating an auth token asynchronously
* var signer = new AWS.RDS.Signer({
* // configure options
* region: 'us-east-1',
* username: 'default',
* hostname: 'db.us-east-1.amazonaws.com',
* port: 8000
* });
* signer.getAuthToken({
* // these options are merged with those defined when creating the signer, overriding in the case of a duplicate option
* // credentials are not specified here or when creating the signer, so default credential provider will be used
* username: 'test' // overriding username
* }, function(err, token) {
* if (err) {
* // handle error
* } else {
* // use token
* }
* });
*
*/
getAuthToken: function getAuthToken(options, callback) {
if (typeof options === 'function' && callback === undefined) {
callback = options;
options = {};
}
var self = this;
var hasCallback = typeof callback === 'function';
// merge options with existing options
options = AWS.util.merge(this.options, options);
// validate options
var optionsValidation = this.validateAuthTokenOptions(options);
if (optionsValidation !== true) {
if (hasCallback) {
return callback(optionsValidation, null);
}
throw optionsValidation;
}

// 15 minutes
var expires = 900;
// create service to generate a request from
service = new AWS.Service({
region: options.region,
credentials: options.credentials,
endpoint: new AWS.Endpoint(options.hostname + ':' + options.port),
paramValidation: false,
signatureVersion: 'v4'
});
// ensure the SDK is using sigv4 signing (config is not enough)
service.api = api;

var request = service.makeRequest();
// add listeners to request to properly build auth token
this.modifyRequestForAuthToken(request, options);

if (hasCallback) {
request.presign(expires, function(err, url) {
if (url) {
url = self.convertUrlToAuthToken(url);
}
callback(err, url);
});
} else {
var url = request.presign(expires);
return this.convertUrlToAuthToken(url);
}
},

/**
* @api private
* Modifies a request to allow the presigner to generate an auth token.
*/
modifyRequestForAuthToken: function modifyRequestForAuthToken(request, options) {
request.on('build', request.buildAsGet);
var httpRequest = request.httpRequest;
httpRequest.body = AWS.util.queryParamsToString({
Action: 'connect',
DBUser: options.username
});
},

/**
* @api private
* Validates that the options passed in contain all the keys with values of the correct type that
* are needed to generate an auth token.
*/
validateAuthTokenOptions: function validateAuthTokenOptions(options) {
// iterate over all keys in options
var message = '';
options = options || {};
for (var key in requiredAuthTokenOptions) {
if (!Object.prototype.hasOwnProperty.call(requiredAuthTokenOptions, key)) {
continue;
}
if (typeof options[key] !== requiredAuthTokenOptions[key]) {
message += 'option \'' + key + '\' should have been type \'' + requiredAuthTokenOptions[key] + '\', was \'' + typeof options[key] + '\'.\n';
}
}
if (message.length) {
return AWS.util.error(new Error(), {
code: 'InvalidParameter',
message: message
});
}
return true;
}
});
9 changes: 7 additions & 2 deletions lib/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,13 @@ AWS.Service = inherit({
getSignerClass: function getSignerClass(request) {
var version;
// get operation authtype if present
var operation = request ? request.service.api.operations[request.operation] : null;
var authtype = operation ? operation.authtype : '';
var operation = null;
var authtype = '';
if (request) {
var operations = request.service.api.operations || {};
operation = operations[request.operation] || null;
authtype = operation ? operation.authtype : '';
}
if (this.config.signatureVersion) {
version = this.config.signatureVersion;
} else if (authtype === 'v4' || authtype === 'v4-unsigned-body') {
Expand Down
2 changes: 1 addition & 1 deletion lib/services/rds.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
var AWS = require('../core');

require('../rds/signer');
/**
* @api private
*/
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@
],
"scripts": {
"test": "npm -s run-script lint && npm -s run-script unit && npm -s run-script buildertest && npm -s run-script browsertest && ([ -f configuration ] && npm -s run-script integration || true)",
"unit": "mocha -- test test/json test/model test/protocol test/query test/services test/signers test/xml test/s3 test/cloudfront test/dynamodb test/polly",
"coverage": "istanbul cover ./node_modules/mocha/bin/_mocha --reporter=lcovonly -- test test/json test/model test/protocol test/query test/services test/signers test/xml test/s3 test/cloudfront test/dynamodb test/polly",
"unit": "mocha -- test test/json test/model test/protocol test/query test/services test/signers test/xml test/s3 test/cloudfront test/dynamodb test/polly test/rds",
"coverage": "istanbul cover ./node_modules/mocha/bin/_mocha --reporter=lcovonly -- test test/json test/model test/protocol test/query test/services test/signers test/xml test/s3 test/cloudfront test/dynamodb test/polly test/rds",
"browsertest": "rake browser:test && karma start",
"buildertest": "mocha --compilers coffee:coffee-script -s 1000 -t 10000 dist-tools/test",
"integration": "cucumber.js",
Expand Down
11 changes: 11 additions & 0 deletions scripts/lib/ts-customizations.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@
]
}
],
"rds": [
{
"path": "lib/rds/signer",
"imports": [
{
"name": "Signer",
"alias": "signer"
}
]
}
],
"s3": [
{
"path": "lib/s3/managed_upload",
Expand Down
Loading

0 comments on commit 35a8f57

Please sign in to comment.