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

[rush-lib] Improve support for S3 storage for the buildCache #2614

Merged
merged 4 commits into from
Apr 17, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 37 additions & 11 deletions apps/rush-lib/src/logic/buildCache/AmazonS3/AmazonS3Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import { IPutFetchOptions, IGetFetchOptions, WebClient } from '../../../utilitie
const CONTENT_HASH_HEADER_NAME: 'x-amz-content-sha256' = 'x-amz-content-sha256';
const DATE_HEADER_NAME: 'x-amz-date' = 'x-amz-date';
const HOST_HEADER_NAME: 'host' = 'host';
const SECURITY_TOKEN_HEADER_NAME: 'x-amz-security-token' = 'x-amz-security-token';

const DEFAULT_S3_REGION: 'us-east-1' = 'us-east-1';

export interface IAmazonS3Credentials {
accessKeyId: string;
secretAccessKey: string;
sessionToken: string | undefined;
}

interface IIsoDateString {
Expand Down Expand Up @@ -49,14 +53,15 @@ export class AmazonS3Client {
return undefined;
}

const splitIndex: number = credentialString.indexOf(':');
if (splitIndex === -1) {
const fields: string[] = credentialString.split(':');
if (fields.length < 2 || fields.length > 3) {
throw new Error('Amazon S3 credential is in an unexpected format.');
}

return {
accessKeyId: credentialString.substring(0, splitIndex),
secretAccessKey: credentialString.substring(splitIndex + 1)
accessKeyId: fields[0],
secretAccessKey: fields[1],
sessionToken: fields[2]
};
}

Expand Down Expand Up @@ -91,15 +96,26 @@ export class AmazonS3Client {
): Promise<fetch.Response> {
const isoDateString: IIsoDateString = this._getIsoDateString();
const bodyHash: string = this._getSha256(body);
const host: string = `${this._s3Bucket}.s3.amazonaws.com`;

const host: string = this._getHost();
const headers: fetch.Headers = new fetch.Headers();
headers.set(DATE_HEADER_NAME, isoDateString.dateTime);
headers.set(CONTENT_HASH_HEADER_NAME, bodyHash);

if (this._credentials) {
// Compute the authorization header. See https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
const signedHeaderNames: string = `${HOST_HEADER_NAME};${CONTENT_HASH_HEADER_NAME};${DATE_HEADER_NAME}`;
const signedHeaderNames: string[] = [HOST_HEADER_NAME, CONTENT_HASH_HEADER_NAME, DATE_HEADER_NAME];
const canonicalHeaders: string[] = [
`${HOST_HEADER_NAME}:${host}`,
`${CONTENT_HASH_HEADER_NAME}:${bodyHash}`,
`${DATE_HEADER_NAME}:${isoDateString.dateTime}`
];

// Handle signing with temporary credentials (via sts:assume-role)
if (this._credentials.sessionToken) {
signedHeaderNames.push(SECURITY_TOKEN_HEADER_NAME);
canonicalHeaders.push(`${SECURITY_TOKEN_HEADER_NAME}:${this._credentials.sessionToken}`);
}

// The canonical request looks like this:
// GET
// /test.txt
Expand All @@ -115,11 +131,9 @@ export class AmazonS3Client {
verb,
`/${objectName}`,
'', // we don't use query strings for these requests
`${HOST_HEADER_NAME}:${host}`,
`${CONTENT_HASH_HEADER_NAME}:${bodyHash}`,
`${DATE_HEADER_NAME}:${isoDateString.dateTime}`,
...canonicalHeaders,
'',
signedHeaderNames,
signedHeaderNames.join(';'),
bodyHash
].join('\n');
const canonicalRequestHash: string = this._getSha256(canonicalRequest);
Expand Down Expand Up @@ -149,6 +163,10 @@ export class AmazonS3Client {
const authorizationHeader: string = `AWS4-HMAC-SHA256 Credential=${this._credentials.accessKeyId}/${scope},SignedHeaders=${signedHeaderNames},Signature=${signature}`;

headers.set('Authorization', authorizationHeader);
if (this._credentials.sessionToken) {
// Handle signing with temporary credentials (via sts:assume-role)
headers.set('X-Amz-Security-Token', this._credentials.sessionToken);
}
}

const webFetchOptions: IGetFetchOptions | IPutFetchOptions = {
Expand Down Expand Up @@ -207,6 +225,14 @@ export class AmazonS3Client {
throw new Error(`Amazon S3 responded with status code ${response.status} (${response.statusText})`);
}

private _getHost(): string {
if (this._s3Region === DEFAULT_S3_REGION) {
return `${this._s3Bucket}.s3.amazonaws.com`;
} else {
return `${this._s3Bucket}.s3-${this._s3Region}.amazonaws.com`;
}
}

/**
* Validates a S3 bucket name.
* {@link https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-s3-bucket-naming-requirements.html}
Expand Down
11 changes: 11 additions & 0 deletions common/changes/@microsoft/rush/s3_2021-04-15-21-04.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "The build cache can now use buckets outside the default region",
iclanton marked this conversation as resolved.
Show resolved Hide resolved
"type": "none"
}
],
"packageName": "@microsoft/rush",
"email": "[email protected]"
}