Skip to content

Commit

Permalink
feat(middleware-bucket-endpoint): support Outposts buckets (#1550)
Browse files Browse the repository at this point in the history
* feat(middleware-bucket-endpoint): generate outpost endpoints

* feat(middleware-bucket-endpoint): refactor and add outposts unit tests

* docs(middleware-bucket-endpoint): add docs
  • Loading branch information
AllanZhengYP authored Oct 6, 2020
1 parent 2d9b200 commit c7915d2
Show file tree
Hide file tree
Showing 7 changed files with 451 additions and 104 deletions.
18 changes: 18 additions & 0 deletions clients/client-s3/S3.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,22 @@ describe("Accesspoint ARN", async () => {
// Sign request with us-west-2 region from bucket access point ARN
expect(result.request.headers.authorization).to.contain("/us-west-2/s3/aws4_request, SignedHeaders=");
});

it("should succeed with outposts ARN", async () => {
const OutpostId = "op-01234567890123456";
const AccountId = "123456789012";
const region = "us-west-2";
const credentials = { accessKeyId: "key", secretAccessKey: "secret" };
const client = new S3({ region: "us-east-1", credentials, useArnRegion: true });
client.middlewareStack.add(endpointValidator, { step: "finalizeRequest", priority: "low" });
const result: any = await client.putObject({
Bucket: `arn:aws:s3-outposts:${region}:${AccountId}:outpost/${OutpostId}/accesspoint/abc-111`,
Key: "key",
Body: "body",
});
expect(result.request.hostname).to.eql(`abc-111-${AccountId}.${OutpostId}.s3-outposts.us-west-2.amazonaws.com`);
expect(result.request.headers["authorization"]).contains(
`Credential=${credentials.accessKeyId}/20200928/${region}/s3-outposts/aws4_request`
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,25 @@ describe("bucketEndpointMiddleware", () => {
});

it("should set signing_region to middleware context if the request will use region from ARN", async () => {
mockBucketHostname.mockReturnValue({
bucketEndpoint: true,
hostname: "myendpoint-123456789012.s3-accesspoint.us-west-2.amazonaws.com",
signingService: "s3-foo",
signingRegion: "us-bar-1",
});
const request = new HttpRequest(requestInput);
previouslyResolvedConfig.useArnRegion.mockReturnValue(true);
const arnRegion = "us-west-2";
mockArnParse.mockReturnValue({ region: arnRegion });
const handlerContext = {} as any;
const handler = bucketEndpointMiddleware(
resolveBucketEndpointConfig({
...previouslyResolvedConfig,
})
)(next, handlerContext);
await handler({
input: { Bucket: `myendpoint-123456789012.s3-accesspoint.${arnRegion}.amazonaws.com` },
input: { Bucket: "Bucket" },
request,
});
expect(handlerContext).toMatchObject({ signing_region: arnRegion });
expect(handlerContext).toMatchObject({ signing_region: "us-bar-1", signing_service: "s3-foo" });
});
});
});
111 changes: 57 additions & 54 deletions packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,67 +15,70 @@ import { bucketHostname } from "./bucketHostname";
import { getPseudoRegion } from "./bucketHostnameUtils";
import { BucketEndpointResolvedConfig } from "./configurations";

export function bucketEndpointMiddleware(options: BucketEndpointResolvedConfig): BuildMiddleware<any, any> {
return <Output extends MetadataBearer>(
next: BuildHandler<any, Output>,
context: HandlerExecutionContext
): BuildHandler<any, Output> => async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
const { Bucket: bucketName } = args.input as { Bucket: string };
let replaceBucketInPath = options.bucketEndpoint;
const request = args.request;
if (HttpRequest.isInstance(request)) {
if (options.bucketEndpoint) {
request.hostname = bucketName;
} else if (validateArn(bucketName)) {
const bucketArn = parseArn(bucketName);
const clientRegion = getPseudoRegion(await options.region());
const { partition, signingRegion } = (await options.regionInfoProvider(clientRegion)) || {};
const useArnRegion = await options.useArnRegion();
const { hostname, bucketEndpoint } = bucketHostname({
bucketName: bucketArn,
baseHostname: request.hostname,
accelerateEndpoint: options.useAccelerateEndpoint,
dualstackEndpoint: options.useDualstackEndpoint,
pathStyleEndpoint: options.forcePathStyle,
tlsCompatible: request.protocol === "https:",
useArnRegion,
clientPartition: partition,
clientSigningRegion: signingRegion,
});
export const bucketEndpointMiddleware = (options: BucketEndpointResolvedConfig): BuildMiddleware<any, any> => <
Output extends MetadataBearer
>(
next: BuildHandler<any, Output>,
context: HandlerExecutionContext
): BuildHandler<any, Output> => async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
const { Bucket: bucketName } = args.input as { Bucket: string };
let replaceBucketInPath = options.bucketEndpoint;
const request = args.request;
if (HttpRequest.isInstance(request)) {
if (options.bucketEndpoint) {
request.hostname = bucketName;
} else if (validateArn(bucketName)) {
const bucketArn = parseArn(bucketName);
const clientRegion = getPseudoRegion(await options.region());
const { partition, signingRegion = clientRegion } = (await options.regionInfoProvider(clientRegion)) || {};
const useArnRegion = await options.useArnRegion();
const { hostname, bucketEndpoint, signingRegion: modifiedSigningRegion, signingService } = bucketHostname({
bucketName: bucketArn,
baseHostname: request.hostname,
accelerateEndpoint: options.useAccelerateEndpoint,
dualstackEndpoint: options.useDualstackEndpoint,
pathStyleEndpoint: options.forcePathStyle,
tlsCompatible: request.protocol === "https:",
useArnRegion,
clientPartition: partition,
clientSigningRegion: signingRegion,
});

// If the request needs to use a region inferred from ARN that different from client region, we need to set
// them in the handler context so the signer will use them
if (useArnRegion && clientRegion !== bucketArn.region) {
context["signing_region"] = bucketArn.region;
}
// If the request needs to use a region or service name inferred from ARN that different from client region, we
// need to set them in the handler context so the signer will use them
if (modifiedSigningRegion && modifiedSigningRegion !== signingRegion) {
context["signing_region"] = modifiedSigningRegion;
}
if (signingService && signingService !== "s3") {
context["signing_service"] = signingService;
}

request.hostname = hostname;
replaceBucketInPath = bucketEndpoint;
} else {
const { hostname, bucketEndpoint } = bucketHostname({
bucketName,
baseHostname: request.hostname,
accelerateEndpoint: options.useAccelerateEndpoint,
dualstackEndpoint: options.useDualstackEndpoint,
pathStyleEndpoint: options.forcePathStyle,
tlsCompatible: request.protocol === "https:",
});
request.hostname = hostname;
replaceBucketInPath = bucketEndpoint;
} else {
const { hostname, bucketEndpoint } = bucketHostname({
bucketName,
baseHostname: request.hostname,
accelerateEndpoint: options.useAccelerateEndpoint,
dualstackEndpoint: options.useDualstackEndpoint,
pathStyleEndpoint: options.forcePathStyle,
tlsCompatible: request.protocol === "https:",
});

request.hostname = hostname;
replaceBucketInPath = bucketEndpoint;
}
request.hostname = hostname;
replaceBucketInPath = bucketEndpoint;
}

if (replaceBucketInPath) {
request.path = request.path.replace(/^(\/)?[^\/]+/, "");
if (request.path === "") {
request.path = "/";
}
if (replaceBucketInPath) {
request.path = request.path.replace(/^(\/)?[^\/]+/, "");
if (request.path === "") {
request.path = "/";
}
}
}

return next({ ...args, request });
};
}
return next({ ...args, request });
};

export const bucketEndpointMiddlewareOptions: RelativeMiddlewareOptions = {
tags: ["BUCKET_ENDPOINT"],
Expand Down
Loading

0 comments on commit c7915d2

Please sign in to comment.