Skip to content

Commit cdcd5c2

Browse files
committed
feat: support @httpApiKeyAuth trait
This adds codegen support for services and operations that use the @httpApiKeyAuth trait. The generated client will have a new `apiKey` config attribute. This value will be injected into the header that's specified in the trait by new middleware that's included in the codegen client.
1 parent e3ef0dd commit cdcd5c2

File tree

7 files changed

+690
-1
lines changed

7 files changed

+690
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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.SymbolProvider;
26+
import software.amazon.smithy.model.Model;
27+
import software.amazon.smithy.model.knowledge.TopDownIndex;
28+
import software.amazon.smithy.model.shapes.OperationShape;
29+
import software.amazon.smithy.model.shapes.ServiceShape;
30+
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait;
31+
import software.amazon.smithy.model.traits.OptionalAuthTrait;
32+
import software.amazon.smithy.typescript.codegen.CodegenUtils;
33+
import software.amazon.smithy.typescript.codegen.TypeScriptSettings;
34+
import software.amazon.smithy.typescript.codegen.TypeScriptWriter;
35+
import software.amazon.smithy.utils.IoUtils;
36+
import software.amazon.smithy.utils.ListUtils;
37+
import software.amazon.smithy.utils.SmithyInternalApi;
38+
39+
/**
40+
* Add config and middleware to support a service with the @httpApiKeyAuth trait.
41+
*/
42+
@SmithyInternalApi
43+
public final class AddHttpApiKeyAuthPlugin implements TypeScriptIntegration {
44+
45+
/**
46+
* Plug into code generation for the client.
47+
*
48+
* This adds the configuration items to the client config and plugs in the
49+
* middleware to operations that need it.
50+
*
51+
* The middleware will inject the client's configured API key into the
52+
* request as defined by the @httpApiKeyAuth trait. If the trait says to
53+
* put the API key into a named header, that header will be used, optionally
54+
* prefixed with a scheme. If the trait says to put the API key into a named
55+
* query parameter, that query parameter will be used.
56+
*/
57+
@Override
58+
public List<RuntimeClientPlugin> getClientPlugins() {
59+
return ListUtils.of(
60+
// Add the config if the service uses HTTP API key authorization
61+
RuntimeClientPlugin.builder()
62+
.inputConfig(Symbol.builder().namespace(
63+
"./" + CodegenUtils.SOURCE_FOLDER + "/middleware/HttpApiKeyAuth", "/"
64+
).name("HttpApiKeyAuthInputConfig").build())
65+
.resolvedConfig(Symbol.builder().namespace(
66+
"./" + CodegenUtils.SOURCE_FOLDER + "/middleware/HttpApiKeyAuth", "/"
67+
).name("HttpApiKeyAuthResolvedConfig").build())
68+
.resolveFunction(Symbol.builder().namespace(
69+
"./" + CodegenUtils.SOURCE_FOLDER + "/middleware/HttpApiKeyAuth", "/"
70+
).name("resolveHttpApiKeyAuthConfig").build())
71+
.servicePredicate((m, s) -> hasHttpApiKeyAuthTrait(s) && !areAllOptionalAuthOperations(m, s))
72+
.build(),
73+
74+
// Add the middleware to operations that use HTTP API key authorization
75+
RuntimeClientPlugin.builder()
76+
.pluginFunction(Symbol.builder()
77+
.namespace("./" + CodegenUtils.SOURCE_FOLDER + "/middleware/HttpApiKeyAuth", "/")
78+
.name("getHttpApiKeyAuthPlugin")
79+
.build())
80+
.additionalPluginFunctionParamsSupplier((m, s, o) -> new HashMap<String, Object>() {{
81+
// It's safe to do getTrait().get() because the operation predicate ensures that the trait exists
82+
// `in` and `name` are required attributes of the trait, `scheme` is optional
83+
put("in", s.getTrait(HttpApiKeyAuthTrait.class).get().getIn().toString());
84+
put("name", s.getTrait(HttpApiKeyAuthTrait.class).get().getName());
85+
put("scheme", s.getTrait(HttpApiKeyAuthTrait.class).get().getScheme().orElse(null));
86+
}})
87+
.operationPredicate((m, s, o) -> hasHttpApiKeyAuthTrait(s)
88+
&& !operationUsesOptionalAuth(m, s, o))
89+
.build()
90+
);
91+
}
92+
93+
@Override
94+
public void writeAdditionalFiles(
95+
TypeScriptSettings settings,
96+
Model model,
97+
SymbolProvider symbolProvider,
98+
BiConsumer<String, Consumer<TypeScriptWriter>> writerFactory
99+
) {
100+
ServiceShape service = settings.getService(model);
101+
102+
// If the service doesn't use HTTP API keys, we don't need to do anything and the generated
103+
// code doesn't need any additional files.
104+
if (!hasHttpApiKeyAuthTrait(service) || areAllOptionalAuthOperations(model, service)) {
105+
return;
106+
}
107+
108+
String noTouchNoticePrefix = "// Please do not touch this file. It's generated from a template in:\n"
109+
+ "// https://github.com/awslabs/smithy-typescript/blob/main/smithy-typescript-codegen/"
110+
+ "src/main/resources/software/amazon/smithy/aws/typescript/codegen/integration/";
111+
112+
// write the middleware source
113+
writerFactory.accept(
114+
Paths.get(CodegenUtils.SOURCE_FOLDER, "middleware", "HttpApiKeyAuth", "index.ts").toString(),
115+
writer -> {
116+
String source = IoUtils.readUtf8Resource(getClass(), "http-api-key-auth.ts");
117+
writer.write("$L$L", noTouchNoticePrefix, "http-api-key-auth.ts");
118+
writer.write("$L", source);
119+
});
120+
121+
// write the middleware tests
122+
writerFactory.accept(
123+
Paths.get(CodegenUtils.SOURCE_FOLDER, "middleware", "HttpApiKeyAuth", "index.spec.ts").toString(),
124+
writer -> {
125+
String source = IoUtils.readUtf8Resource(getClass(), "http-api-key-auth.spec.ts");
126+
writer.write("$L$L", noTouchNoticePrefix, "http-api-key-auth.spec.ts");
127+
writer.write("$L", source);
128+
});
129+
}
130+
131+
/**
132+
* Check if the service has the @httpApiKeyAuth trait.
133+
*
134+
* @param service the service shape
135+
*
136+
* @return true if the service has the @httpApiKeyAuth trait
137+
*/
138+
private static boolean hasHttpApiKeyAuthTrait(ServiceShape service) {
139+
return service.hasTrait(HttpApiKeyAuthTrait.class);
140+
}
141+
142+
// 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
143+
private static boolean areAllOptionalAuthOperations(Model model, ServiceShape service) {
144+
TopDownIndex topDownIndex = TopDownIndex.of(model);
145+
Set<OperationShape> operations = topDownIndex.getContainedOperations(service);
146+
for (OperationShape operation : operations) {
147+
if (!operationUsesOptionalAuth(model, service, operation)) {
148+
return false;
149+
}
150+
}
151+
return true;
152+
}
153+
154+
// 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
155+
private static boolean hasOptionalAuthOperation(Model model, ServiceShape service) {
156+
TopDownIndex topDownIndex = TopDownIndex.of(model);
157+
Set<OperationShape> operations = topDownIndex.getContainedOperations(service);
158+
for (OperationShape operation : operations) {
159+
if (operationUsesOptionalAuth(model, service, operation)) {
160+
return true;
161+
}
162+
}
163+
return false;
164+
}
165+
166+
/**
167+
* Check if the operation has @optionalAuth, implying it doesn't need the middleware.
168+
*
169+
* @param model the model
170+
* @param service the service shape
171+
* @param operation the operation shape
172+
*
173+
* @return true if the operation has the @optionalAuth trait
174+
*/
175+
private static boolean operationUsesOptionalAuth(Model model, ServiceShape service, OperationShape operation) {
176+
return operation.hasTrait(OptionalAuthTrait.class);
177+
}
178+
}
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
software.amazon.smithy.typescript.codegen.integration.AddEventStreamDependency
2-
software.amazon.smithy.typescript.codegen.integration.AddChecksumRequiredDependency
2+
software.amazon.smithy.typescript.codegen.integration.AddChecksumRequiredDependency
3+
software.amazon.smithy.typescript.codegen.integration.AddHttpApiKeyAuthPlugin
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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 throw an error 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 expect(
73+
handler({
74+
input: {},
75+
request: new HttpRequest({}),
76+
})
77+
).rejects.toThrow("query parameter is not supported");
78+
79+
expect(mockNextHandler.mock.calls.length).toEqual(0);
80+
});
81+
82+
it("should throw an error if the api key has not been set", async () => {
83+
const middleware = httpApiKeyAuthMiddleware(
84+
{},
85+
{
86+
in: "header",
87+
name: "auth",
88+
scheme: "scheme",
89+
}
90+
);
91+
92+
const handler = middleware(mockNextHandler, {});
93+
94+
await expect(
95+
handler({
96+
input: {},
97+
request: new HttpRequest({}),
98+
})
99+
).rejects.toThrow("no API key was provided");
100+
101+
expect(mockNextHandler.mock.calls.length).toEqual(0);
102+
});
103+
104+
it("should skip if the request is not an HttpRequest", async () => {
105+
const middleware = httpApiKeyAuthMiddleware(
106+
{},
107+
{
108+
in: "header",
109+
name: "Authorization",
110+
}
111+
);
112+
113+
const handler = middleware(mockNextHandler, {});
114+
115+
await handler({
116+
input: {},
117+
request: {},
118+
});
119+
120+
expect(mockNextHandler.mock.calls.length).toEqual(1);
121+
});
122+
123+
it("should set the API key in the lower-cased named header", async () => {
124+
const middleware = httpApiKeyAuthMiddleware(
125+
{
126+
apiKey: "exampleApiKey",
127+
},
128+
{
129+
in: "header",
130+
name: "Authorization",
131+
}
132+
);
133+
134+
const handler = middleware(mockNextHandler, {});
135+
136+
await handler({
137+
input: {},
138+
request: new HttpRequest({}),
139+
});
140+
141+
expect(mockNextHandler.mock.calls.length).toEqual(1);
142+
expect(
143+
mockNextHandler.mock.calls[0][0].request.headers.authorization
144+
).toBe("exampleApiKey");
145+
});
146+
147+
it("should set the API key in the named header with the provided scheme", async () => {
148+
const middleware = httpApiKeyAuthMiddleware(
149+
{
150+
apiKey: "exampleApiKey",
151+
},
152+
{
153+
in: "header",
154+
name: "authorization",
155+
scheme: "exampleScheme",
156+
}
157+
);
158+
const handler = middleware(mockNextHandler, {});
159+
160+
await handler({
161+
input: {},
162+
request: new HttpRequest({}),
163+
});
164+
165+
expect(mockNextHandler.mock.calls.length).toEqual(1);
166+
expect(
167+
mockNextHandler.mock.calls[0][0].request.headers.authorization
168+
).toBe("exampleScheme exampleApiKey");
169+
});
170+
});
171+
});

0 commit comments

Comments
 (0)