Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

storage: add getSignedPolicy function to generate a signed policy #502

Closed
wants to merge 10 commits into from
142 changes: 142 additions & 0 deletions lib/storage/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,148 @@ File.prototype.getSignedUrl = function(options, callback) {
});
};

/**
* Get a signed policy document to allow user to upload data
* with a POST.
*
* *[Reference](http://goo.gl/JWJEkG).*
*
* @throws {Error} if an expiration timestamp from the past is given or
* option parameter does not respect the expected format
*
* @param {object} options - Configuration object.
* @param {object} options.expiration - Timestamp (seconds since epoch)
* when this policy will expire.
* @param {object[][]=} options.equals - Array of request parameters and
* their expected value (e.g. [["$<field>", "<value>"]]). Values are

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

* translated into equality constraints in the conditions
* field of the policy document (e.g. ["eq", "$<field>", "<value>"]).
* If only one equality condition is to be specified, options.equals
* can be a one-dimensional array (e.g. ["$<field>", "<value>"])
* @param {object[][]=} options.startsWith - Array of request parameters and
* their expected prefixes (e.g. [["$<field>", "<value>"]]). Values are
* translated into starts-with constraints in the conditions field
* of the policy document (e.g. ["starts-with", "$<field>", "<value>"])
* If only one prefix condition is to be specified, options.startsWith
* can be a one-dimensional array (e.g. ["$<field>", "<value>"])
* @param {string=} options.acl - ACL for the object from possibly predefined
* ACLs
* @param {string=} options.successRedirect - The URL to which the user
* client is redirected if the upload is successfull
* @param {string=} options.successStatus - The status of the Google Storage
* response if the upload is successfull (must be string)
* @param {object=} options.contentLengthRange - Object providing
* minimum (options.contentLengthRange.min) and maximum
* (options.contentLengthRange.max) value for the request's
* content length
*
* @example
* file.getSignedPolicy({
* equals: ["$Content-Type", "image/jpeg"],
* contentLengthRange: {min: 0, max: 1024},
* expiration: Math.round(Date.now() / 1000) + (60 * 60 * 24 * 14) // 2 weeks.
* }, function(err, policy) {
* // policy.string: the policy document, plain text
* // policy.base64: the policy document, base64
* // policy.signature: the policy signature, base64
* });
*/
File.prototype.getSignedPolicy = function(options, callback) {
if (options.expiration < Math.floor(Date.now() / 1000)) {
throw new Error('An expiration date cannot be in the past.');
}

var expirationString = new Date(options.expiration).toISOString();
var conditions = [
['eq', '$key', this.name],
{
bucket: this.bucket.name
},
];

if (util.is(options.equals, 'array')) {
if (!util.is(options.equals[0], 'array')) {
options.equals = [options.equals];
}
options.equals.forEach(function(condition) {
if (!util.is(condition, 'array') || condition.length !== 2) {
throw new Error('Equals condition must be an array of 2 elements.');
}
conditions.push(['eq', condition[0], condition[1]]);
});
}

This comment was marked as spam.

This comment was marked as spam.


if (util.is(options.startsWith, 'array')) {
if (!util.is(options.startsWith[0], 'array')) {
options.startsWith = [options.startsWith];
}
options.startsWith.forEach(function(condition) {
if (!util.is(condition, 'array') || condition.length !== 2) {
throw new Error('StartsWith condition must be an array of 2 elements.');
}
conditions.push(['starts-with', condition[0], condition[1]]);
});
}

if (options.acl) {
conditions.push({
acl: options.acl
});
}

if (options.successRedirect) {
conditions.push({
success_action_redirect: options.successRedirect
});
}

if (options.successStatus) {
conditions.push({
success_action_status: options.successStatus
});
}

if (options.contentLengthRange) {
var min = options.contentLengthRange.min;
var max = options.contentLengthRange.max;
if (!util.is(min, 'number') || !util.is(max, 'number')) {
throw new Error(
'ContentLengthRange must have numeric min and max fields.'
);
}
conditions.push(['content-length-range', min, max]);
}

var policy = {
expiration: expirationString,
conditions: conditions
};

var makeAuthorizedRequest_ = this.bucket.storage.makeAuthorizedRequest_;

makeAuthorizedRequest_.getCredentials(function(err, credentials) {
if (err) {
callback(err);
return;
}

var sign = crypto.createSign('RSA-SHA256');
var policyString = JSON.stringify(policy);
var policyBase64 = new Buffer(policyString).toString('base64');

sign.update(policyBase64);

var signature = sign.sign(credentials.private_key, 'base64');

callback(null, {
string: policyString,
base64: policyBase64,
signature: signature
});
});
};

This comment was marked as spam.



/**
* Set the file's metadata.
*
Expand Down
200 changes: 200 additions & 0 deletions test/storage/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,206 @@ describe('File', function() {
});
});

describe('getSignedPolicy', function() {
var credentials = require('../testdata/privateKeyFile.json');

beforeEach(function() {
var storage = bucket.storage;
storage.makeAuthorizedRequest_.getCredentials = function(callback) {
callback(null, credentials);
};
});

it('should create a signed policy', function(done) {
file.getSignedPolicy({
expiration: Math.round(Date.now() / 1000) + 5
}, function(err, signedPolicy) {
assert.ifError(err);
assert.equal(typeof signedPolicy.string, 'string');
assert.equal(typeof signedPolicy.base64, 'string');
assert.equal(typeof signedPolicy.signature, 'string');
done();
});
});

it('should add key equality condition', function(done) {
file.getSignedPolicy({
expiration: Math.round(Date.now() / 1000) + 5
}, function(err, signedPolicy) {
var conditionString = '[\"eq\",\"$key\",\"'+file.name+'\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should add ACL condtion', function(done) {
file.getSignedPolicy({
expiration: Math.round(Date.now() / 1000) + 5,
acl: '<acl>'
}, function(err, signedPolicy) {
var conditionString = '{\"acl\":\"<acl>\"}';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

describe('expiration', function() {
it('should ISO encode expiration', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
var expireDate = new Date(expiration);
file.getSignedPolicy({
expiration: expiration
}, function(err, signedPolicy) {
assert.ifError(err);
assert(signedPolicy.string.indexOf(expireDate.toISOString()) > -1);
done();
});
});

it('should throw if a date from the past is given', function() {
var expirationTimestamp = Math.floor(Date.now() / 1000) - 1;
assert.throws(function() {
file.getSignedPolicy({
expiration: expirationTimestamp
}, function() {});
}, /cannot be in the past/);
});
});

describe('equality condition', function() {
it('should add equality conditions (array of arrays)', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
equals: [['$<field>', '<value>']]
}, function(err, signedPolicy) {
var conditionString = '[\"eq\",\"$<field>\",\"<value>\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

This comment was marked as spam.


it('should add equality condition (array)', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
equals: ['$<field>', '<value>']
}, function(err, signedPolicy) {
var conditionString = '[\"eq\",\"$<field>\",\"<value>\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should throw if equal condition is not an array', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
equals: [{}]
}, function() {});
}, /Equals condition must be an array/);
});

it('should throw if equal condition length is not 2', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
equals: [['1', '2', '3']]
}, function() {});
}, /Equals condition must be an array of 2 elements/);
});
});

describe('prefix conditions', function() {
it('should add prefix conditions (array of arrays)', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
startsWith: [['$<field>', '<value>']]
}, function(err, signedPolicy) {
var conditionString = '[\"starts-with\",\"$<field>\",\"<value>\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should add prefix condition (array)', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
startsWith: ['$<field>', '<value>']
}, function(err, signedPolicy) {
var conditionString = '[\"starts-with\",\"$<field>\",\"<value>\"]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should throw if prexif condition is not an array', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
startsWith: [{}]
}, function() {});
}, /StartsWith condition must be an array/);
});

it('should throw if prefix condition length is not 2', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
startsWith: [['1', '2', '3']]
}, function() {});
}, /StartsWith condition must be an array of 2 elements/);
});
});

describe('content length', function() {
it('should add content length condition', function(done) {
var expiration = Math.round(Date.now() / 1000) + 5;
file.getSignedPolicy({
expiration: expiration,
contentLengthRange: {min: 0, max: 1}
}, function(err, signedPolicy) {
var conditionString = '[\"content-length-range\",0,1]';
assert.ifError(err);
assert(signedPolicy.string.indexOf(conditionString) > -1);
done();
});
});

it('should throw if content length has no min', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
contentLengthRange: [{max: 1}]
}, function() {});
}, /ContentLengthRange must have numeric min and max fields/);
});

it('should throw if content length has no max', function() {
var expiration = Math.round(Date.now() / 1000) + 5;
assert.throws(function() {
file.getSignedPolicy({
expiration: expiration,
contentLengthRange: [{min: 0}]
}, function() {});
}, /ContentLengthRange must have numeric min and max fields/);
});
});
});

describe('setMetadata', function() {
var metadata = { fake: 'metadata' };

Expand Down