Skip to content

Commit c7915d2

Browse files
authored
feat(middleware-bucket-endpoint): support Outposts buckets (#1550)
* feat(middleware-bucket-endpoint): generate outpost endpoints * feat(middleware-bucket-endpoint): refactor and add outposts unit tests * docs(middleware-bucket-endpoint): add docs
1 parent 2d9b200 commit c7915d2

File tree

7 files changed

+451
-104
lines changed

7 files changed

+451
-104
lines changed

clients/client-s3/S3.spec.ts

+18
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,22 @@ describe("Accesspoint ARN", async () => {
7373
// Sign request with us-west-2 region from bucket access point ARN
7474
expect(result.request.headers.authorization).to.contain("/us-west-2/s3/aws4_request, SignedHeaders=");
7575
});
76+
77+
it("should succeed with outposts ARN", async () => {
78+
const OutpostId = "op-01234567890123456";
79+
const AccountId = "123456789012";
80+
const region = "us-west-2";
81+
const credentials = { accessKeyId: "key", secretAccessKey: "secret" };
82+
const client = new S3({ region: "us-east-1", credentials, useArnRegion: true });
83+
client.middlewareStack.add(endpointValidator, { step: "finalizeRequest", priority: "low" });
84+
const result: any = await client.putObject({
85+
Bucket: `arn:aws:s3-outposts:${region}:${AccountId}:outpost/${OutpostId}/accesspoint/abc-111`,
86+
Key: "key",
87+
Body: "body",
88+
});
89+
expect(result.request.hostname).to.eql(`abc-111-${AccountId}.${OutpostId}.s3-outposts.us-west-2.amazonaws.com`);
90+
expect(result.request.headers["authorization"]).contains(
91+
`Credential=${credentials.accessKeyId}/20200928/${region}/s3-outposts/aws4_request`
92+
);
93+
});
7694
});

packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.spec.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -165,21 +165,25 @@ describe("bucketEndpointMiddleware", () => {
165165
});
166166

167167
it("should set signing_region to middleware context if the request will use region from ARN", async () => {
168+
mockBucketHostname.mockReturnValue({
169+
bucketEndpoint: true,
170+
hostname: "myendpoint-123456789012.s3-accesspoint.us-west-2.amazonaws.com",
171+
signingService: "s3-foo",
172+
signingRegion: "us-bar-1",
173+
});
168174
const request = new HttpRequest(requestInput);
169175
previouslyResolvedConfig.useArnRegion.mockReturnValue(true);
170-
const arnRegion = "us-west-2";
171-
mockArnParse.mockReturnValue({ region: arnRegion });
172176
const handlerContext = {} as any;
173177
const handler = bucketEndpointMiddleware(
174178
resolveBucketEndpointConfig({
175179
...previouslyResolvedConfig,
176180
})
177181
)(next, handlerContext);
178182
await handler({
179-
input: { Bucket: `myendpoint-123456789012.s3-accesspoint.${arnRegion}.amazonaws.com` },
183+
input: { Bucket: "Bucket" },
180184
request,
181185
});
182-
expect(handlerContext).toMatchObject({ signing_region: arnRegion });
186+
expect(handlerContext).toMatchObject({ signing_region: "us-bar-1", signing_service: "s3-foo" });
183187
});
184188
});
185189
});

packages/middleware-bucket-endpoint/src/bucketEndpointMiddleware.ts

+57-54
Original file line numberDiff line numberDiff line change
@@ -15,67 +15,70 @@ import { bucketHostname } from "./bucketHostname";
1515
import { getPseudoRegion } from "./bucketHostnameUtils";
1616
import { BucketEndpointResolvedConfig } from "./configurations";
1717

18-
export function bucketEndpointMiddleware(options: BucketEndpointResolvedConfig): BuildMiddleware<any, any> {
19-
return <Output extends MetadataBearer>(
20-
next: BuildHandler<any, Output>,
21-
context: HandlerExecutionContext
22-
): BuildHandler<any, Output> => async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
23-
const { Bucket: bucketName } = args.input as { Bucket: string };
24-
let replaceBucketInPath = options.bucketEndpoint;
25-
const request = args.request;
26-
if (HttpRequest.isInstance(request)) {
27-
if (options.bucketEndpoint) {
28-
request.hostname = bucketName;
29-
} else if (validateArn(bucketName)) {
30-
const bucketArn = parseArn(bucketName);
31-
const clientRegion = getPseudoRegion(await options.region());
32-
const { partition, signingRegion } = (await options.regionInfoProvider(clientRegion)) || {};
33-
const useArnRegion = await options.useArnRegion();
34-
const { hostname, bucketEndpoint } = bucketHostname({
35-
bucketName: bucketArn,
36-
baseHostname: request.hostname,
37-
accelerateEndpoint: options.useAccelerateEndpoint,
38-
dualstackEndpoint: options.useDualstackEndpoint,
39-
pathStyleEndpoint: options.forcePathStyle,
40-
tlsCompatible: request.protocol === "https:",
41-
useArnRegion,
42-
clientPartition: partition,
43-
clientSigningRegion: signingRegion,
44-
});
18+
export const bucketEndpointMiddleware = (options: BucketEndpointResolvedConfig): BuildMiddleware<any, any> => <
19+
Output extends MetadataBearer
20+
>(
21+
next: BuildHandler<any, Output>,
22+
context: HandlerExecutionContext
23+
): BuildHandler<any, Output> => async (args: BuildHandlerArguments<any>): Promise<BuildHandlerOutput<Output>> => {
24+
const { Bucket: bucketName } = args.input as { Bucket: string };
25+
let replaceBucketInPath = options.bucketEndpoint;
26+
const request = args.request;
27+
if (HttpRequest.isInstance(request)) {
28+
if (options.bucketEndpoint) {
29+
request.hostname = bucketName;
30+
} else if (validateArn(bucketName)) {
31+
const bucketArn = parseArn(bucketName);
32+
const clientRegion = getPseudoRegion(await options.region());
33+
const { partition, signingRegion = clientRegion } = (await options.regionInfoProvider(clientRegion)) || {};
34+
const useArnRegion = await options.useArnRegion();
35+
const { hostname, bucketEndpoint, signingRegion: modifiedSigningRegion, signingService } = bucketHostname({
36+
bucketName: bucketArn,
37+
baseHostname: request.hostname,
38+
accelerateEndpoint: options.useAccelerateEndpoint,
39+
dualstackEndpoint: options.useDualstackEndpoint,
40+
pathStyleEndpoint: options.forcePathStyle,
41+
tlsCompatible: request.protocol === "https:",
42+
useArnRegion,
43+
clientPartition: partition,
44+
clientSigningRegion: signingRegion,
45+
});
4546

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

52-
request.hostname = hostname;
53-
replaceBucketInPath = bucketEndpoint;
54-
} else {
55-
const { hostname, bucketEndpoint } = bucketHostname({
56-
bucketName,
57-
baseHostname: request.hostname,
58-
accelerateEndpoint: options.useAccelerateEndpoint,
59-
dualstackEndpoint: options.useDualstackEndpoint,
60-
pathStyleEndpoint: options.forcePathStyle,
61-
tlsCompatible: request.protocol === "https:",
62-
});
56+
request.hostname = hostname;
57+
replaceBucketInPath = bucketEndpoint;
58+
} else {
59+
const { hostname, bucketEndpoint } = bucketHostname({
60+
bucketName,
61+
baseHostname: request.hostname,
62+
accelerateEndpoint: options.useAccelerateEndpoint,
63+
dualstackEndpoint: options.useDualstackEndpoint,
64+
pathStyleEndpoint: options.forcePathStyle,
65+
tlsCompatible: request.protocol === "https:",
66+
});
6367

64-
request.hostname = hostname;
65-
replaceBucketInPath = bucketEndpoint;
66-
}
68+
request.hostname = hostname;
69+
replaceBucketInPath = bucketEndpoint;
70+
}
6771

68-
if (replaceBucketInPath) {
69-
request.path = request.path.replace(/^(\/)?[^\/]+/, "");
70-
if (request.path === "") {
71-
request.path = "/";
72-
}
72+
if (replaceBucketInPath) {
73+
request.path = request.path.replace(/^(\/)?[^\/]+/, "");
74+
if (request.path === "") {
75+
request.path = "/";
7376
}
7477
}
78+
}
7579

76-
return next({ ...args, request });
77-
};
78-
}
80+
return next({ ...args, request });
81+
};
7982

8083
export const bucketEndpointMiddlewareOptions: RelativeMiddlewareOptions = {
8184
tags: ["BUCKET_ENDPOINT"],

0 commit comments

Comments
 (0)