Skip to content

Commit a1a37e6

Browse files
glbtrivikr
andauthored
feat: support @httpApiKeyAuth trait (#473)
Co-authored-by: Trivikram Kamat <[email protected]>
1 parent 943c84a commit a1a37e6

File tree

9 files changed

+760
-1
lines changed

9 files changed

+760
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.smithy.typescript.codegen.integration;
17+
18+
import java.nio.file.Paths;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Set;
22+
import java.util.function.BiConsumer;
23+
import java.util.function.Consumer;
24+
import software.amazon.smithy.codegen.core.Symbol;
25+
import software.amazon.smithy.codegen.core.SymbolDependency;
26+
import software.amazon.smithy.codegen.core.SymbolProvider;
27+
import software.amazon.smithy.model.Model;
28+
import software.amazon.smithy.model.knowledge.ServiceIndex;
29+
import software.amazon.smithy.model.knowledge.TopDownIndex;
30+
import software.amazon.smithy.model.shapes.OperationShape;
31+
import software.amazon.smithy.model.shapes.ServiceShape;
32+
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait;
33+
import software.amazon.smithy.model.traits.OptionalAuthTrait;
34+
import software.amazon.smithy.typescript.codegen.CodegenUtils;
35+
import software.amazon.smithy.typescript.codegen.TypeScriptSettings;
36+
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
37+
import software.amazon.smithy.utils.IoUtils;
38+
import software.amazon.smithy.utils.ListUtils;
39+
import software.amazon.smithy.utils.SmithyInternalApi;
40+
41+
/**
42+
* Add config and middleware to support a service with the @httpApiKeyAuth trait.
43+
*/
44+
@SmithyInternalApi
45+
public final class AddHttpApiKeyAuthPlugin implements TypeScriptIntegration {
46+
47+
/**
48+
* Plug into code generation for the client.
49+
*
50+
* This adds the configuration items to the client config and plugs in the
51+
* middleware to operations that need it.
52+
*
53+
* The middleware will inject the client's configured API key into the
54+
* request as defined by the @httpApiKeyAuth trait. If the trait says to
55+
* put the API key into a named header, that header will be used, optionally
56+
* prefixed with a scheme. If the trait says to put the API key into a named
57+
* query parameter, that query parameter will be used.
58+
*/
59+
@Override
60+
public List<RuntimeClientPlugin> getClientPlugins() {
61+
return ListUtils.of(
62+
// Add the config if the service uses HTTP API key authorization.
63+
RuntimeClientPlugin.builder()
64+
.inputConfig(Symbol.builder()
65+
.namespace("./" + CodegenUtils.SOURCE_FOLDER + "/middleware/HttpApiKeyAuth", "/")
66+
.name("HttpApiKeyAuthInputConfig")
67+
.build())
68+
.resolvedConfig(Symbol.builder()
69+
.namespace("./" + CodegenUtils.SOURCE_FOLDER + "/middleware/HttpApiKeyAuth", "/")
70+
.name("HttpApiKeyAuthResolvedConfig")
71+
.build())
72+
.resolveFunction(Symbol.builder()
73+
.namespace("./" + CodegenUtils.SOURCE_FOLDER + "/middleware/HttpApiKeyAuth", "/")
74+
.name("resolveHttpApiKeyAuthConfig")
75+
.build())
76+
.servicePredicate((m, s) -> hasEffectiveHttpApiKeyAuthTrait(m, s))
77+
.build(),
78+
79+
// Add the middleware to operations that use HTTP API key authorization.
80+
RuntimeClientPlugin.builder()
81+
.pluginFunction(Symbol.builder()
82+
.namespace("./" + CodegenUtils.SOURCE_FOLDER + "/middleware/HttpApiKeyAuth", "/")
83+
.name("getHttpApiKeyAuthPlugin")
84+
.build())
85+
.additionalPluginFunctionParamsSupplier((m, s, o) -> new HashMap<String, Object>() {{
86+
// It's safe to do expectTrait() because the operation predicate ensures that the trait
87+
// exists `in` and `name` are required attributes of the trait, `scheme` is optional.
88+
put("in", s.expectTrait(HttpApiKeyAuthTrait.class).getIn().toString());
89+
put("name", s.expectTrait(HttpApiKeyAuthTrait.class).getName());
90+
s.expectTrait(HttpApiKeyAuthTrait.class).getScheme().ifPresent(scheme ->
91+
put("scheme", scheme));
92+
}})
93+
.operationPredicate((m, s, o) -> ServiceIndex.of(m).getEffectiveAuthSchemes(s, o)
94+
.keySet()
95+
.contains(HttpApiKeyAuthTrait.ID)
96+
&& !o.hasTrait(OptionalAuthTrait.class))
97+
.build()
98+
);
99+
}
100+
101+
@Override
102+
public void writeAdditionalFiles(
103+
TypeScriptSettings settings,
104+
Model model,
105+
SymbolProvider symbolProvider,
106+
BiConsumer<String, Consumer<TypeScriptWriter>> writerFactory
107+
) {
108+
ServiceShape service = settings.getService(model);
109+
110+
// If the service doesn't use HTTP API keys, we don't need to do anything and the generated
111+
// code doesn't need any additional files.
112+
if (!hasEffectiveHttpApiKeyAuthTrait(model, service)) {
113+
return;
114+
}
115+
116+
String noTouchNoticePrefix = "// Please do not touch this file. It's generated from a template in:\n"
117+
+ "// https://github.com/awslabs/smithy-typescript/blob/main/smithy-typescript-codegen/"
118+
+ "src/main/resources/software/amazon/smithy/aws/typescript/codegen/integration/";
119+
120+
// Write the middleware source.
121+
writerFactory.accept(
122+
Paths.get(CodegenUtils.SOURCE_FOLDER, "middleware", "HttpApiKeyAuth", "index.ts").toString(),
123+
writer -> {
124+
String source = IoUtils.readUtf8Resource(getClass(), "http-api-key-auth.ts");
125+
writer.write("$L$L", noTouchNoticePrefix, "http-api-key-auth.ts");
126+
writer.write("$L", source);
127+
});
128+
129+
// Write the middleware tests.
130+
writerFactory.accept(
131+
Paths.get(CodegenUtils.SOURCE_FOLDER, "middleware", "HttpApiKeyAuth", "index.spec.ts").toString(),
132+
writer -> {
133+
writer.addDependency(SymbolDependency.builder()
134+
.dependencyType("devDependencies")
135+
.packageName("@types/jest")
136+
.version("latest")
137+
.build());
138+
139+
String source = IoUtils.readUtf8Resource(getClass(), "http-api-key-auth.spec.ts");
140+
writer.write("$L$L", noTouchNoticePrefix, "http-api-key-auth.spec.ts");
141+
writer.write("$L", source);
142+
});
143+
}
144+
145+
// The service has the effective trait if it's in the "effective auth schemes" response
146+
// AND if not all of the operations have the optional auth trait.
147+
private static boolean hasEffectiveHttpApiKeyAuthTrait(Model model, ServiceShape service) {
148+
return ServiceIndex.of(model).getEffectiveAuthSchemes(service)
149+
.keySet()
150+
.contains(HttpApiKeyAuthTrait.ID)
151+
&& !areAllOptionalAuthOperations(model, service);
152+
}
153+
154+
155+
// This is derived from https://github.com/aws/aws-sdk-js-v3/blob/main/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddAwsAuthPlugin.java.
156+
private static boolean areAllOptionalAuthOperations(Model model, ServiceShape service) {
157+
TopDownIndex topDownIndex = TopDownIndex.of(model);
158+
Set<OperationShape> operations = topDownIndex.getContainedOperations(service);
159+
ServiceIndex index = ServiceIndex.of(model);
160+
161+
for (OperationShape operation : operations) {
162+
if (index.getEffectiveAuthSchemes(service, operation).isEmpty()
163+
|| !operation.hasTrait(OptionalAuthTrait.class)) {
164+
return false;
165+
}
166+
}
167+
return true;
168+
}
169+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
software.amazon.smithy.typescript.codegen.integration.AddEventStreamDependency
22
software.amazon.smithy.typescript.codegen.integration.AddChecksumRequiredDependency
3-
software.amazon.smithy.typescript.codegen.integration.AddDefaultsModeDependency
3+
software.amazon.smithy.typescript.codegen.integration.AddDefaultsModeDependency
4+
software.amazon.smithy.typescript.codegen.integration.AddHttpApiKeyAuthPlugin
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import {
2+
getHttpApiKeyAuthPlugin,
3+
httpApiKeyAuthMiddleware,
4+
resolveHttpApiKeyAuthConfig,
5+
} from "./index";
6+
import { HttpRequest } from "@aws-sdk/protocol-http";
7+
8+
describe("resolveHttpApiKeyAuthConfig", () => {
9+
it("should return the input unchanged", () => {
10+
const config = {
11+
apiKey: "exampleApiKey",
12+
};
13+
expect(resolveHttpApiKeyAuthConfig(config)).toEqual(config);
14+
});
15+
});
16+
17+
describe("getHttpApiKeyAuthPlugin", () => {
18+
it("should apply the middleware to the stack", () => {
19+
const plugin = getHttpApiKeyAuthPlugin(
20+
{
21+
apiKey: "exampleApiKey",
22+
},
23+
{
24+
in: "query",
25+
name: "key",
26+
}
27+
);
28+
29+
const mockAdd = jest.fn();
30+
const mockOther = jest.fn();
31+
32+
// TODO there's got to be a better way to do this mocking
33+
plugin.applyToStack({
34+
add: mockAdd,
35+
// We don't expect any of these others to be called.
36+
addRelativeTo: mockOther,
37+
concat: mockOther,
38+
resolve: mockOther,
39+
applyToStack: mockOther,
40+
use: mockOther,
41+
clone: mockOther,
42+
remove: mockOther,
43+
removeByTag: mockOther,
44+
});
45+
46+
expect(mockAdd.mock.calls.length).toEqual(1);
47+
expect(mockOther.mock.calls.length).toEqual(0);
48+
});
49+
});
50+
51+
describe("httpApiKeyAuthMiddleware", () => {
52+
describe("returned middleware function", () => {
53+
const mockNextHandler = jest.fn();
54+
55+
beforeEach(() => {
56+
jest.clearAllMocks();
57+
});
58+
59+
it("should set the query parameter if the location is `query`", async () => {
60+
const middleware = httpApiKeyAuthMiddleware(
61+
{
62+
apiKey: "exampleApiKey",
63+
},
64+
{
65+
in: "query",
66+
name: "key",
67+
}
68+
);
69+
70+
const handler = middleware(mockNextHandler, {});
71+
72+
await handler({
73+
input: {},
74+
request: new HttpRequest({}),
75+
});
76+
77+
expect(mockNextHandler.mock.calls.length).toEqual(1);
78+
expect(
79+
mockNextHandler.mock.calls[0][0].request.query.key
80+
).toBe("exampleApiKey");
81+
});
82+
83+
it("should skip if the api key has not been set", async () => {
84+
const middleware = httpApiKeyAuthMiddleware(
85+
{},
86+
{
87+
in: "header",
88+
name: "auth",
89+
scheme: "scheme",
90+
}
91+
);
92+
93+
const handler = middleware(mockNextHandler, {});
94+
95+
await handler({
96+
input: {},
97+
request: new HttpRequest({}),
98+
});
99+
100+
expect(mockNextHandler.mock.calls.length).toEqual(1);
101+
});
102+
103+
it("should skip if the request is not an HttpRequest", async () => {
104+
const middleware = httpApiKeyAuthMiddleware(
105+
{},
106+
{
107+
in: "header",
108+
name: "Authorization",
109+
}
110+
);
111+
112+
const handler = middleware(mockNextHandler, {});
113+
114+
await handler({
115+
input: {},
116+
request: {},
117+
});
118+
119+
expect(mockNextHandler.mock.calls.length).toEqual(1);
120+
});
121+
122+
it("should set the API key in the lower-cased named header", async () => {
123+
const middleware = httpApiKeyAuthMiddleware(
124+
{
125+
apiKey: "exampleApiKey",
126+
},
127+
{
128+
in: "header",
129+
name: "Authorization",
130+
}
131+
);
132+
133+
const handler = middleware(mockNextHandler, {});
134+
135+
await handler({
136+
input: {},
137+
request: new HttpRequest({}),
138+
});
139+
140+
expect(mockNextHandler.mock.calls.length).toEqual(1);
141+
expect(
142+
mockNextHandler.mock.calls[0][0].request.headers.authorization
143+
).toBe("exampleApiKey");
144+
});
145+
146+
it("should set the API key in the named header with the provided scheme", async () => {
147+
const middleware = httpApiKeyAuthMiddleware(
148+
{
149+
apiKey: "exampleApiKey",
150+
},
151+
{
152+
in: "header",
153+
name: "authorization",
154+
scheme: "exampleScheme",
155+
}
156+
);
157+
const handler = middleware(mockNextHandler, {});
158+
159+
await handler({
160+
input: {},
161+
request: new HttpRequest({}),
162+
});
163+
164+
expect(mockNextHandler.mock.calls.length).toEqual(1);
165+
expect(
166+
mockNextHandler.mock.calls[0][0].request.headers.authorization
167+
).toBe("exampleScheme exampleApiKey");
168+
});
169+
});
170+
});

0 commit comments

Comments
 (0)