Skip to content

Commit

Permalink
feat: v4 POST with signed policy (#1102)
Browse files Browse the repository at this point in the history
* alias getSignedPolicy to getSignedPolicyV2

* getSignedPolicyV4

* fix conformance-test

* test/fix expires

* update tests

* update system-test

* docs: v4 signed policy

* export types

* fix alias test

* fix PolicyInput in v4SignedUrl.ts

* acl is in fields

* rename cname option to bucketBoundHostname

* add context

* execute signed policy

* chore: rename getSignedPolicy to generateSignedPostPolicy (#1125)

* chore: rename getSignedPolicy to generateSignedPostPolicy

* npm run fix

* fix any

* update to conformance-test@24a5e23 - includes escaping special characters

* EOF
  • Loading branch information
jkwlui authored Mar 26, 2020
1 parent 5561452 commit a3d5b88
Show file tree
Hide file tree
Showing 7 changed files with 984 additions and 159 deletions.
30 changes: 29 additions & 1 deletion conformance-test/test-data/v4SignedUrl.json
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,34 @@
},
"expectedDecodedPolicy": "{\"conditions\":[{\"success_action_redirect\":\"http://www.google.com/\"},{\"key\":\"test-object\"},{\"x-goog-date\":\"20200123T043530Z\"},{\"x-goog-credential\":\"test-iam-credentials@dummy-project-id.iam.gserviceaccount.com/20200123/auto/storage/goog4_request\"},{\"x-goog-algorithm\":\"GOOG4-RSA-SHA256\"}],\"expiration\":\"2020-01-23T04:35:40Z\"}"
}
},
{
"description": "POST Policy Character Escaping",
"policyInput": {
"scheme": "https",
"bucket": "rsaposttest-1579902671-6ldm6caw4se52vrx",
"object": "$test-object-é",
"expiration": 10,
"timestamp": "2020-01-23T04:35:30Z",
"fields": {
"success_action_redirect": "http://www.google.com/",
"x-goog-meta": "$test-object-é-metadata"
}
},
"policyOutput": {
"url": "https://storage.googleapis.com/rsaposttest-1579902671-6ldm6caw4se52vrx/",
"fields" : {
"key": "$test-object-é",
"success_action_redirect": "http://www.google.com/",
"x-goog-meta": "$test-object-é-metadata",
"x-goog-algorithm": "GOOG4-RSA-SHA256",
"x-goog-credential": "test-iam-credentials@dummy-project-id.iam.gserviceaccount.com/20200123/auto/storage/goog4_request",
"x-goog-date": "20200123T043530Z",
"x-goog-signature": "05eb19ea4ece513cbb2bc6a92c9bc82de6be46943fb4703df3f7b26e6033f90a194e2444e6c3166e9585ca468b5727702aa2696e5cca54677c047f7734119ea0d635404d6a5e577b737ffd5414059cd1b508aa99cfad592d9228f1bf47d7df3ffd73bcae6af6d8d83f7f50b4ccbf6e6c0798d2d9923a7e18c8888e2519fcf09d174b7015581a7de021964eeb9d27293213686d80d825332819c4e98d4ab2c5237f352840993e22a02a41d827ce6a4a294e84a33bf051519fdcbf982f2ad932f58714608c4b5a1f89d5e322d194f5e29fa4160fce771008320ac4e659adeead36aa07fe26a96e52e809436b7bd169256d6613c135148fdee6926caaef65817dc2",
"policy": "eyJjb25kaXRpb25zIjpbeyJzdWNjZXNzX2FjdGlvbl9yZWRpcmVjdCI6Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbS8ifSx7IngtZ29vZy1tZXRhIjoiJHRlc3Qtb2JqZWN0LVx1MDBlOS1tZXRhZGF0YSJ9LHsia2V5IjoiJHRlc3Qtb2JqZWN0LVx1MDBlOSJ9LHsieC1nb29nLWRhdGUiOiIyMDIwMDEyM1QwNDM1MzBaIn0seyJ4LWdvb2ctY3JlZGVudGlhbCI6InRlc3QtaWFtLWNyZWRlbnRpYWxzQGR1bW15LXByb2plY3QtaWQuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20vMjAyMDAxMjMvYXV0by9zdG9yYWdlL2dvb2c0X3JlcXVlc3QifSx7IngtZ29vZy1hbGdvcml0aG0iOiJHT09HNC1SU0EtU0hBMjU2In1dLCJleHBpcmF0aW9uIjoiMjAyMC0wMS0yM1QwNDozNTo0MFoifQ=="
},
"expectedDecodedPolicy": "{\"conditions\":[{\"success_action_redirect\":\"http://www.google.com/\"},{\"x-goog-meta\":\"$test-object-\u00e9-metadata\"},{\"key\":\"$test-object-\u00e9\"},{\"x-goog-date\":\"20200123T043530Z\"},{\"x-goog-credential\":\"test-iam-credentials@dummy-project-id.iam.gserviceaccount.com/20200123/auto/storage/goog4_request\"},{\"x-goog-algorithm\":\"GOOG4-RSA-SHA256\"}],\"expiration\":\"2020-01-23T04:35:40Z\"}"
}
}
]
}
}
244 changes: 140 additions & 104 deletions conformance-test/v4SignedUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import * as path from 'path';
import * as sinon from 'sinon';
import * as querystring from 'querystring';

import {Storage, GetSignedUrlConfig} from '../src/';
import {
Storage,
GetSignedUrlConfig,
GenerateSignedPostPolicyV4Options,
} from '../src/';
import * as url from 'url';

export enum UrlStyle {
Expand Down Expand Up @@ -57,6 +61,8 @@ interface PolicyInput {
object: string;
expiration: number;
timestamp: string;
urlStyle?: UrlStyle;
bucketBoundHostname?: string;
conditions?: Conditions;
fields?: {[key: string]: string};
}
Expand All @@ -70,6 +76,7 @@ interface Conditions {
interface PolicyOutput {
url: string;
fields: {[key: string]: string};
expectedDecodedPolicy: string;
}

interface FileAction {
Expand All @@ -86,13 +93,10 @@ const testFile = fs.readFileSync(
);

// tslint:disable-next-line no-any
const testCases: any[] = JSON.parse(testFile).signingV4Tests;
const v4SignedUrlCases: V4SignedURLTestCase[] = testCases.filter(
testCase => testCase.expectedUrl
);
const v4SignedPolicyCases: V4SignedPolicyTestCase[] = testCases.filter(
testCase => testCase.policyInput
);
const testCases = JSON.parse(testFile);
const v4SignedUrlCases: V4SignedURLTestCase[] = testCases.signingV4Tests;
const v4SignedPolicyCases: V4SignedPolicyTestCase[] =
testCases.postPolicyV4Tests;

const SERVICE_ACCOUNT = path.join(
__dirname,
Expand All @@ -101,112 +105,144 @@ const SERVICE_ACCOUNT = path.join(

const storage = new Storage({keyFilename: SERVICE_ACCOUNT});

describe('v4 signed url', () => {
v4SignedUrlCases.forEach(testCase => {
it(testCase.description, async () => {
const NOW = new Date(testCase.timestamp);

const fakeTimer = sinon.useFakeTimers(NOW);
const bucket = storage.bucket(testCase.bucket);
const expires = NOW.valueOf() + testCase.expiration * 1000;
const version = 'v4' as 'v4';
const domain = testCase.bucketBoundHostname
? `${testCase.scheme}://${testCase.bucketBoundHostname}`
: undefined;
const {cname, virtualHostedStyle} = parseUrlStyle(
testCase.urlStyle,
domain
);
const extensionHeaders = testCase.headers;
const queryParams = testCase.queryParameters;
const baseConfig = {
extensionHeaders,
version,
expires,
cname,
virtualHostedStyle,
queryParams,
};
let signedUrl: string;

if (testCase.object) {
const file = bucket.file(testCase.object);

const action = ({
GET: 'read',
POST: 'resumable',
PUT: 'write',
DELETE: 'delete',
} as FileAction)[testCase.method];

[signedUrl] = await file.getSignedUrl({
action,
...baseConfig,
} as GetSignedUrlConfig);
} else {
// bucket operation
const action = ({
GET: 'list',
} as BucketAction)[testCase.method];

[signedUrl] = await bucket.getSignedUrl({
action,
...baseConfig,
});
}

const expected = new url.URL(testCase.expectedUrl);
const actual = new url.URL(signedUrl);

assert.strictEqual(actual.origin, expected.origin);
assert.strictEqual(actual.pathname, expected.pathname);
// Order-insensitive comparison of query params
assert.deepStrictEqual(
querystring.parse(actual.search),
querystring.parse(expected.search)
);

fakeTimer.restore();
describe('v4 conformance test', () => {
describe('v4 signed url', () => {
v4SignedUrlCases.forEach(testCase => {
it(testCase.description, async () => {
const NOW = new Date(testCase.timestamp);

const fakeTimer = sinon.useFakeTimers(NOW);
const bucket = storage.bucket(testCase.bucket);
const expires = NOW.valueOf() + testCase.expiration * 1000;
const version = 'v4' as 'v4';
const origin = testCase.bucketBoundHostname
? `${testCase.scheme}://${testCase.bucketBoundHostname}`
: undefined;
const {bucketBoundHostname, virtualHostedStyle} = parseUrlStyle(
testCase.urlStyle,
origin
);
const extensionHeaders = testCase.headers;
const queryParams = testCase.queryParameters;
const baseConfig = {
extensionHeaders,
version,
expires,
cname: bucketBoundHostname,
virtualHostedStyle,
queryParams,
};
let signedUrl: string;

if (testCase.object) {
const file = bucket.file(testCase.object);

const action = ({
GET: 'read',
POST: 'resumable',
PUT: 'write',
DELETE: 'delete',
} as FileAction)[testCase.method];

[signedUrl] = await file.getSignedUrl({
action,
...baseConfig,
} as GetSignedUrlConfig);
} else {
// bucket operation
const action = ({
GET: 'list',
} as BucketAction)[testCase.method];

[signedUrl] = await bucket.getSignedUrl({
action,
...baseConfig,
});
}

const expected = new url.URL(testCase.expectedUrl);
const actual = new url.URL(signedUrl);

assert.strictEqual(actual.origin, expected.origin);
assert.strictEqual(actual.pathname, expected.pathname);
// Order-insensitive comparison of query params
assert.deepStrictEqual(
querystring.parse(actual.search),
querystring.parse(expected.search)
);

fakeTimer.restore();
});
});
});
});

// tslint:disable-next-line ban
describe.skip('v4 signed policy', () => {
v4SignedPolicyCases.forEach(testCase => {
// TODO: implement parsing v4 signed policy tests
it(testCase.description, async () => {
// const input = testCase.policyInput;
// const NOW = new Date(input.timestamp);
// const fakeTimer = sinon.useFakeTimers(NOW);
// const bucket = storage.bucket(input.bucket);
// const expires = NOW.valueOf() + input.expiration * 1000;
// const options = {};
// const fields = input.fields || {};
// // fields that Node.js supports as argument to method.
// const acl = fields.acl;
// delete fields.acl;
// const successActionStatus = fields.success_action_status;
// delete fields.successActionStatus;
// const successActionRedirect = fields.success_action_redirect;
// delete fields.successActionRedirect;
// const conditions = input.conditions || {} as Conditions;
// // conditions that Node.js support as argument to method.
// const startsWith = conditions.startsWith;
// let contentLengthMin
// if (conditions.contentLengthRange) {
// }
// fakeTimer.restore();
describe('v4 signed policy', () => {
v4SignedPolicyCases.forEach(testCase => {
it(testCase.description, async () => {
const input = testCase.policyInput;
const NOW = new Date(input.timestamp);
const fakeTimer = sinon.useFakeTimers(NOW);
const bucket = storage.bucket(input.bucket);
const expires = NOW.valueOf() + input.expiration * 1000;
const options: GenerateSignedPostPolicyV4Options = {
expires,
};

const conditions = [];
if (input.conditions) {
if (input.conditions.startsWith) {
const variable = input.conditions.startsWith[0];
const prefix = input.conditions.startsWith[1];
conditions.push(['starts-with', variable, prefix]);
}

if (input.conditions.contentLengthRange) {
const min = input.conditions.contentLengthRange[0];
const max = input.conditions.contentLengthRange[1];
conditions.push(['content-length-range', min, max]);
}
}

const origin = input.bucketBoundHostname
? `${input.scheme}://${input.bucketBoundHostname}`
: undefined;
const {bucketBoundHostname, virtualHostedStyle} = parseUrlStyle(
input.urlStyle,
origin
);
options.virtualHostedStyle = virtualHostedStyle;
options.bucketBoundHostname = bucketBoundHostname;
options.fields = input.fields;
options.conditions = conditions;

const file = bucket.file(input.object);
const [policy] = await file.generateSignedPostPolicyV4(options);

assert.strictEqual(policy.url, testCase.policyOutput.url);
const outputFields = testCase.policyOutput.fields;
const decodedPolicy = Buffer.from(
policy.fields.policy,
'base64'
).toString();
assert.deepStrictEqual(
decodedPolicy,
testCase.policyOutput.expectedDecodedPolicy
);

assert.deepStrictEqual(outputFields, testCase.policyOutput.fields);

fakeTimer.restore();
});
});
});
});

function parseUrlStyle(
style?: UrlStyle,
domain?: string
): {cname?: string; virtualHostedStyle?: boolean} {
origin?: string
): {bucketBoundHostname?: string; virtualHostedStyle?: boolean} {
if (style === UrlStyle.BUCKET_BOUND_HOSTNAME) {
return {cname: domain};
return {bucketBoundHostname: origin};
} else if (style === UrlStyle.VIRTUAL_HOSTED_STYLE) {
return {virtualHostedStyle: true};
} else {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,13 @@
"@types/tmp": "0.1.0",
"@types/uuid": "^7.0.0",
"@types/xdg-basedir": "^2.0.0",
"c8": "^7.0.0",
"codecov": "^3.0.0",
"eslint": "^6.0.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-node": "^11.0.0",
"eslint-plugin-prettier": "^3.0.0",
"form-data": "^3.0.0",
"grpc": "^1.22.2",
"gts": "^1.0.0",
"jsdoc": "^3.6.2",
Expand All @@ -111,7 +113,6 @@
"nock": "~12.0.0",
"node-fetch": "^2.2.0",
"normalize-newline": "^3.0.0",
"c8": "^7.0.0",
"prettier": "^1.7.0",
"proxyquire": "^2.1.3",
"sinon": "^9.0.0",
Expand Down
Loading

0 comments on commit a3d5b88

Please sign in to comment.