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

feat(s3-control): support S3 Outposts control plane #1553

Merged
merged 9 commits into from
Oct 7, 2020
3 changes: 3 additions & 0 deletions packages/middleware-sdk-s3-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@
},
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/middleware-bucket-endpoint": "1.0.0-gamma.8",
"@aws-sdk/protocol-http": "1.0.0-gamma.7",
"@aws-sdk/types": "1.0.0-gamma.6",
"@aws-sdk/util-arn-parser": "1.0.0-gamma.3",
"tslib": "^1.8.0"
},
"devDependencies": {
"@aws-sdk/middleware-stack": "1.0.0-gamma.7",
"@types/jest": "^26.0.4",
"jest": "^26.1.0",
"typescript": "~4.0.2"
Expand Down
36 changes: 36 additions & 0 deletions packages/middleware-sdk-s3-control/src/configurations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Provider, RegionInfoProvider } from "@aws-sdk/types";
export { NODE_USE_ARN_REGION_CONFIG_OPTIONS } from "@aws-sdk/middleware-bucket-endpoint";

export interface S3ControlInputConfig {
/**
* Enables IPv6/IPv4 dualstack endpoint. When a DNS lookup is performed on an endpoint of this type, it returns an “A” record with an IPv4 address and an “AAAA” record with an IPv6 address. In most cases the network stack in the client environment will automatically prefer the AAAA record and make a connection using the IPv6 address. Note, however, that currently on Windows, the IPv4 address will be preferred.
*/
useDualstackEndpoint?: boolean;
/**
* Whether to override the request region with the region inferred from requested resource's ARN. Defaults to false
*/
useArnRegion?: boolean | Provider<boolean>;
}

interface PreviouslyResolved {
region: Provider<string>;
regionInfoProvider: RegionInfoProvider;
}

export interface S3ControlResolvedConfig {
useDualstackEndpoint: boolean;
useArnRegion: Provider<boolean>;
region: Provider<string>;
regionInfoProvider: RegionInfoProvider;
}

export function resolveS3ControlConfig<T>(
input: T & PreviouslyResolved & S3ControlInputConfig
): T & S3ControlResolvedConfig {
const { useDualstackEndpoint = false, useArnRegion = false } = input;
return {
...input,
useDualstackEndpoint,
useArnRegion: typeof useArnRegion === "function" ? useArnRegion : () => Promise.resolve(useArnRegion),
};
}
5 changes: 5 additions & 0 deletions packages/middleware-sdk-s3-control/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const CONTEXT_OUTPOST_ID = "outpost_id";
export const CONTEXT_ACCOUNT_ID = "account_id";
export const CONTEXT_ARN_REGION = "outpost_arn_region";
export const CONTEXT_SIGNING_SERVICE = "signing_service";
export const CONTEXT_SIGNING_REGION = "signing_region";
9 changes: 9 additions & 0 deletions packages/middleware-sdk-s3-control/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from "./configurations";
export {
getProcessArnablesPlugin,
parseOutpostArnablesMiddleaware,
parseOutpostArnablesMiddleawareOptions,
updateArnablesRequestMiddleware,
updateArnablesRequestMiddlewareOptions,
} from "./process-arnables-plugin";
export * from "./redirect-from-postid";
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { getProcessArnablesPlugin } from "./plugin";
export { parseOutpostArnablesMiddleaware, parseOutpostArnablesMiddleawareOptions } from "./parse-outpost-arnables";
export {
updateArnablesRequestMiddleware,
updateArnablesRequestMiddlewareOptions,
replaceHostname,
} from "./update-arnables-request";
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
getArnResources as getS3AccesspointArnResources,
getPseudoRegion,
validateAccountId,
validateNoDualstack,
validateNoFIPS,
validateOutpostService,
validatePartition,
validateRegion,
} from "@aws-sdk/middleware-bucket-endpoint";
import { InitializeHandlerOptions, InitializeMiddleware } from "@aws-sdk/types";
import { ARN, parse as parseArn, validate as validateArn } from "@aws-sdk/util-arn-parser";

import { S3ControlResolvedConfig } from "../configurations";
import { CONTEXT_ARN_REGION, CONTEXT_OUTPOST_ID, CONTEXT_SIGNING_REGION, CONTEXT_SIGNING_SERVICE } from "../constants";

type ArnableInput = {
Name?: string;
Bucket?: string;
AccountId?: string;
};

export const parseOutpostArnablesMiddleaware = (
options: S3ControlResolvedConfig
): InitializeMiddleware<ArnableInput, any> => (next, context) => async (args) => {
const { input } = args;

const parameter: "Name" | "Bucket" | undefined =
input.Name && validateArn(input.Name) ? "Name" : input.Bucket && validateArn(input.Bucket) ? "Bucket" : undefined;
if (!parameter) return next(args);

const clientRegion = await options.region();
const { regionInfoProvider } = options;
const useArnRegion = await options.useArnRegion();
const baseRegion = getPseudoRegion(clientRegion);
const { partition: clientPartition, signingRegion = baseRegion } = (await regionInfoProvider(baseRegion))!;
const validatorOptions: ValidateOutpostsArnOptions = {
useDualstackEndpoint: options.useDualstackEndpoint,
clientRegion,
clientPartition,
signingRegion,
useArnRegion,
};
let arn: ARN;
if (parameter === "Name") {
arn = parseArn(input.Name!);
validateOutpostsArn(arn, validatorOptions);
const { outpostId, accesspointName } = parseOutpostsAccessPointArnResource(arn.resource);
input.Name = accesspointName;
context[CONTEXT_OUTPOST_ID] = outpostId;
} else {
arn = parseArn(input.Bucket!);
validateOutpostsArn(arn, validatorOptions);
const { outpostId, bucketName } = parseOutpostBucketArnResource(arn.resource);
input.Bucket = bucketName;
context[CONTEXT_OUTPOST_ID] = outpostId;
}
context[CONTEXT_SIGNING_SERVICE] = arn.service; // s3-outposts
context[CONTEXT_SIGNING_REGION] = useArnRegion ? arn.region : signingRegion;

if (!input.AccountId) {
input.AccountId = arn.accountId;
} else if (input.AccountId !== arn.accountId) {
throw new Error(`AccountId is incompatible with account id inferred from ${parameter}`);
}

if (useArnRegion) context[CONTEXT_ARN_REGION] = arn.region;

return next(args);
};

export const parseOutpostArnablesMiddleawareOptions: InitializeHandlerOptions = {
step: "initialize",
tags: ["CONVERT_ARN", "OUTPOST_BUCKET_ARN", "OUTPOST_ACCESS_POINT_ARN", "OUTPOST"],
name: "parseOutpostArnablesMiddleaware",
};

type ValidateOutpostsArnOptions = {
clientRegion: string;
signingRegion: string;
clientPartition: string;
useArnRegion: boolean;
useDualstackEndpoint: boolean;
};
/**
AllanZhengYP marked this conversation as resolved.
Show resolved Hide resolved
* validate ARN with 's3-outposts' in the service section of the ARN:
* ARN supplied to 'Name' parameter should comply template:
* arn:{partition}:s3-outposts:{region}:{accountId}:outpost/{outpostId}/accesspoint/{accesspointName}
* ARN supplied to 'Bucket' parameter should comply template:
* arn:{partition}:s3-outposts:{region}:{accountId}:outpost/{outpostId}/bucket/{bucketName}
*/
const validateOutpostsArn = (
arn: ARN,
{ clientRegion, signingRegion, clientPartition, useArnRegion, useDualstackEndpoint }: ValidateOutpostsArnOptions
) => {
const { service, partition, accountId, region } = arn;
validateOutpostService(service);
validatePartition(partition, { clientPartition });
validateAccountId(accountId);
validateRegion(region, {
useArnRegion,
clientRegion,
clientSigningRegion: signingRegion,
});
validateNoDualstack(useDualstackEndpoint);
if (!useArnRegion) validateNoFIPS(clientRegion);
};

const parseOutpostsAccessPointArnResource = (
resource: string
): {
outpostId: string;
accesspointName: string;
} => {
const { outpostId, accesspointName } = getS3AccesspointArnResources(resource);
if (!outpostId) {
throw new Error("ARN resource should begin with 'outpost'");
}
return {
outpostId,
accesspointName,
};
};

const parseOutpostBucketArnResource = (
resource: string
): {
outpostId: string;
bucketName: string;
} => {
const delimiter = resource.includes(":") ? ":" : "/";
const [resourceType, ...rest] = resource.split(delimiter);
if (resourceType === "outpost") {
// Parse outpost ARN
if (!rest[0] || rest[1] !== "bucket" || !rest[2] || rest.length !== 3) {
throw new Error(
`Outpost Bucket ARN should have resource outpost${delimiter}{outpostId}${delimiter}bucket${delimiter}{bucketName}`
);
}
const [outpostId, _, bucketName] = rest;
return { outpostId, bucketName };
} else {
throw new Error(`ARN resource should begin with 'outpost${delimiter}'`);
}
};