Skip to content

Commit 86f5e47

Browse files
authored
feat: implement smithy reference arch for identity and auth resolution (#469)
1 parent bc219e6 commit 86f5e47

File tree

57 files changed

+3129
-1191
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+3129
-1191
lines changed

Diff for: auth/auth.go

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package auth defines protocol-agnostic authentication types for smithy
2+
// clients.
3+
package auth

Diff for: auth/identity.go

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/aws/smithy-go"
8+
)
9+
10+
// Identity contains information that identifies who the user making the
11+
// request is.
12+
type Identity interface {
13+
Expiration() time.Time
14+
}
15+
16+
// IdentityResolver defines the interface through which an Identity is
17+
// retrieved.
18+
type IdentityResolver interface {
19+
GetIdentity(context.Context, smithy.Properties) (Identity, error)
20+
}
21+
22+
// IdentityResolverOptions defines the interface through which an entity can be
23+
// queried to retrieve an IdentityResolver for a given auth scheme.
24+
type IdentityResolverOptions interface {
25+
GetIdentityResolver(schemeID string) IdentityResolver
26+
}
27+
28+
// AnonymousIdentity is a sentinel to indicate no identity.
29+
type AnonymousIdentity struct{}
30+
31+
var _ Identity = (*AnonymousIdentity)(nil)
32+
33+
// Expiration returns the zero value for time, as anonymous identity never
34+
// expires.
35+
func (*AnonymousIdentity) Expiration() time.Time {
36+
return time.Time{}
37+
}
38+
39+
// AnonymousIdentityResolver returns AnonymousIdentity.
40+
type AnonymousIdentityResolver struct{}
41+
42+
var _ IdentityResolver = (*AnonymousIdentityResolver)(nil)
43+
44+
// GetIdentity returns AnonymousIdentity.
45+
func (*AnonymousIdentityResolver) GetIdentity(_ context.Context, _ smithy.Properties) (Identity, error) {
46+
return &AnonymousIdentity{}, nil
47+
}

Diff for: auth/option.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package auth
2+
3+
import "github.com/aws/smithy-go"
4+
5+
type (
6+
authOptionsKey struct{}
7+
)
8+
9+
// Option represents a possible authentication method for an operation.
10+
type Option struct {
11+
SchemeID string
12+
IdentityProperties smithy.Properties
13+
SignerProperties smithy.Properties
14+
}
15+
16+
// GetAuthOptions gets auth Options from Properties.
17+
func GetAuthOptions(p *smithy.Properties) ([]*Option, bool) {
18+
v, ok := p.Get(authOptionsKey{}).([]*Option)
19+
return v, ok
20+
}
21+
22+
// SetAuthOptions sets auth Options on Properties.
23+
func SetAuthOptions(p *smithy.Properties, options []*Option) {
24+
p.Set(authOptionsKey{}, options)
25+
}

Diff for: auth/scheme_id.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package auth
2+
3+
// Anonymous
4+
const (
5+
SchemeIDAnonymous = "smithy.api#noAuth"
6+
)
7+
8+
// HTTP auth schemes
9+
const (
10+
SchemeIDHTTPBasic = "smithy.api#httpBasicAuth"
11+
SchemeIDHTTPDigest = "smithy.api#httpDigestAuth"
12+
SchemeIDHTTPBearer = "smithy.api#httpBearerAuth"
13+
SchemeIDHTTPAPIKey = "smithy.api#httpApiKeyAuth"
14+
)
15+
16+
// AWS auth schemes
17+
const (
18+
SchemeIDSigV4 = "aws.auth#sigv4"
19+
SchemeIDSigV4A = "aws.auth#sigv4a"
20+
)

Diff for: codegen/smithy-go-codegen/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extra["moduleName"] = "software.amazon.smithy.go.codegen"
2121

2222
dependencies {
2323
api("software.amazon.smithy:smithy-codegen-core:$smithyVersion")
24+
api("software.amazon.smithy:smithy-aws-traits:$smithyVersion")
2425
implementation("software.amazon.smithy:smithy-waiters:$smithyVersion")
2526
api("com.atlassian.commonmark:commonmark:0.15.2")
2627
api("org.jsoup:jsoup:1.14.1")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/*
2+
* Copyright 2023 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.go.codegen;
17+
18+
import static software.amazon.smithy.go.codegen.GoWriter.goDocTemplate;
19+
import static software.amazon.smithy.go.codegen.GoWriter.goTemplate;
20+
21+
import java.util.Comparator;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.function.Predicate;
25+
import java.util.stream.Collectors;
26+
import software.amazon.smithy.go.codegen.auth.AuthSchemeResolverGenerator;
27+
import software.amazon.smithy.go.codegen.integration.AuthSchemeDefinition;
28+
import software.amazon.smithy.go.codegen.integration.ConfigField;
29+
import software.amazon.smithy.go.codegen.integration.ProtocolGenerator;
30+
import software.amazon.smithy.go.codegen.integration.auth.AnonymousDefinition;
31+
import software.amazon.smithy.model.knowledge.ServiceIndex;
32+
import software.amazon.smithy.model.shapes.ShapeId;
33+
import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
34+
import software.amazon.smithy.utils.MapUtils;
35+
36+
/**
37+
* Implements codegen for service client config.
38+
*/
39+
public class ClientOptions implements GoWriter.Writable {
40+
public static final String NAME = "Options";
41+
42+
private final ProtocolGenerator.GenerationContext context;
43+
private final ApplicationProtocol protocol;
44+
45+
private final List<ConfigField> fields;
46+
private final Map<ShapeId, AuthSchemeDefinition> authSchemes;
47+
48+
public ClientOptions(ProtocolGenerator.GenerationContext context, ApplicationProtocol protocol) {
49+
this.context = context;
50+
this.protocol = protocol;
51+
52+
this.fields = context.getIntegrations().stream()
53+
.flatMap(it -> it.getClientPlugins(context.getModel(), context.getService()).stream())
54+
.flatMap(it -> it.getConfigFields().stream())
55+
.distinct()
56+
.sorted(Comparator.comparing(ConfigField::getName))
57+
.toList();
58+
this.authSchemes = context.getIntegrations().stream()
59+
.flatMap(it -> it.getClientPlugins(context.getModel(), context.getService()).stream())
60+
.flatMap(it -> it.getAuthSchemeDefinitions().entrySet().stream())
61+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
62+
}
63+
64+
@Override
65+
public void accept(GoWriter writer) {
66+
writer.write(generate());
67+
}
68+
69+
private GoWriter.Writable generate() {
70+
var apiOptionsDocs = goDocTemplate(
71+
"Set of options to modify how an operation is invoked. These apply to all operations "
72+
+ "invoked for this client. Use functional options on operation call to modify this "
73+
+ "list for per operation behavior."
74+
);
75+
return goTemplate("""
76+
$protocolTypes:W
77+
78+
type $options:L struct {
79+
$apiOptionsDocs:W
80+
APIOptions []func($stack:P) error
81+
82+
$fields:W
83+
84+
$protocolFields:W
85+
}
86+
87+
$copy:W
88+
89+
$getIdentityResolver:W
90+
91+
$helpers:W
92+
""",
93+
MapUtils.of(
94+
"protocolTypes", generateProtocolTypes(),
95+
"apiOptionsDocs", apiOptionsDocs,
96+
"options", NAME,
97+
"stack", SmithyGoTypes.Middleware.Stack,
98+
"fields", GoWriter.ChainWritable.of(fields.stream().map(this::writeField).toList()).compose(),
99+
"protocolFields", generateProtocolFields(),
100+
"copy", generateCopy(),
101+
"getIdentityResolver", generateGetIdentityResolver(),
102+
"helpers", generateHelpers()
103+
));
104+
}
105+
106+
private GoWriter.Writable generateProtocolTypes() {
107+
ensureSupportedProtocol();
108+
return goTemplate("""
109+
type HTTPClient interface {
110+
Do($P) ($P, error)
111+
}
112+
""", GoStdlibTypes.Net.Http.Request, GoStdlibTypes.Net.Http.Response);
113+
}
114+
115+
private GoWriter.Writable writeField(ConfigField field) {
116+
GoWriter.Writable docs = writer -> {
117+
field.getDocumentation().ifPresent(writer::writeDocs);
118+
field.getDeprecated().ifPresent(s -> {
119+
if (field.getDocumentation().isPresent()) {
120+
writer.writeDocs("");
121+
}
122+
writer.writeDocs(String.format("Deprecated: %s", s));
123+
});
124+
};
125+
return goTemplate("""
126+
$W
127+
$L $P
128+
""", docs, field.getName(), field.getType());
129+
}
130+
131+
private GoWriter.Writable generateProtocolFields() {
132+
ensureSupportedProtocol();
133+
return goTemplate("""
134+
$1W
135+
HTTPClient HTTPClient
136+
137+
$2W
138+
AuthSchemeResolver $4L
139+
140+
$3W
141+
AuthSchemes []$5T
142+
""",
143+
goDocTemplate("The HTTP client to invoke API calls with. "
144+
+ "Defaults to client's default HTTP implementation if nil."),
145+
goDocTemplate("The auth scheme resolver which determines how to authenticate for each operation."),
146+
goDocTemplate("The list of auth schemes supported by the client."),
147+
AuthSchemeResolverGenerator.INTERFACE_NAME,
148+
SmithyGoTypes.Transport.Http.AuthScheme);
149+
}
150+
151+
private GoWriter.Writable generateCopy() {
152+
return goTemplate("""
153+
// Copy creates a clone where the APIOptions list is deep copied.
154+
func (o $1L) Copy() $1L {
155+
to := o
156+
to.APIOptions = make([]func($2P) error, len(o.APIOptions))
157+
copy(to.APIOptions, o.APIOptions)
158+
159+
return to
160+
}
161+
""", NAME, SmithyGoTypes.Middleware.Stack);
162+
}
163+
164+
private GoWriter.Writable generateGetIdentityResolver() {
165+
return goTemplate("""
166+
func (o $L) GetIdentityResolver(schemeID string) $T {
167+
$W
168+
$W
169+
return nil
170+
}
171+
""",
172+
NAME,
173+
SmithyGoTypes.Auth.IdentityResolver,
174+
GoWriter.ChainWritable.of(
175+
ServiceIndex.of(context.getModel())
176+
.getEffectiveAuthSchemes(context.getService()).keySet().stream()
177+
.filter(authSchemes::containsKey)
178+
.map(trait -> generateGetIdentityResolverMapping(trait, authSchemes.get(trait)))
179+
.toList()
180+
).compose(false),
181+
generateGetIdentityResolverMapping(NoAuthTrait.ID, new AnonymousDefinition()));
182+
}
183+
184+
private GoWriter.Writable generateGetIdentityResolverMapping(ShapeId schemeId, AuthSchemeDefinition scheme) {
185+
return goTemplate("""
186+
if schemeID == $S {
187+
return $W
188+
}""", schemeId.toString(), scheme.generateOptionsIdentityResolver());
189+
}
190+
191+
private GoWriter.Writable generateHelpers() {
192+
return writer -> {
193+
writer.write("""
194+
$W
195+
func WithAPIOptions(optFns ...func($P) error) func(*Options) {
196+
return func (o *Options) {
197+
o.APIOptions = append(o.APIOptions, optFns...)
198+
}
199+
}
200+
""",
201+
goDocTemplate(
202+
"WithAPIOptions returns a functional option for setting the Client's APIOptions option."
203+
),
204+
SmithyGoTypes.Middleware.Stack);
205+
206+
fields.stream().filter(ConfigField::getWithHelper).filter(ConfigField::isDeprecated)
207+
.forEach(configField -> {
208+
writer.writeDocs(configField.getDeprecated().get());
209+
writeHelper(writer, configField);
210+
});
211+
212+
fields.stream().filter(ConfigField::getWithHelper).filter(Predicate.not(ConfigField::isDeprecated))
213+
.forEach(configField -> {
214+
writer.writeDocs(
215+
String.format(
216+
"With%s returns a functional option for setting the Client's %s option.",
217+
configField.getName(), configField.getName()));
218+
writeHelper(writer, configField);
219+
});
220+
};
221+
}
222+
223+
private void writeHelper(GoWriter writer, ConfigField configField) {
224+
writer.write("""
225+
func With$1L(v $2P) func(*Options) {
226+
return func(o *Options) {
227+
o.$1L = v
228+
}
229+
}
230+
""", configField.getName(), configField.getType());
231+
}
232+
233+
private void ensureSupportedProtocol() {
234+
if (!protocol.isHttpProtocol()) {
235+
throw new UnsupportedOperationException("Protocols other than HTTP are not yet implemented: " + protocol);
236+
}
237+
}
238+
}

Diff for: codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/CodegenVisitor.java

+21
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ void execute() {
247247
protocolGenerator.generateEndpointResolution(context);
248248
});
249249

250+
writers.useFileWriter("auth.go", settings.getModuleName(), writer -> {
251+
ProtocolGenerator.GenerationContext context = contextBuilder.writer(writer).build();
252+
protocolGenerator.generateAuth(context);
253+
});
254+
250255
writers.useFileWriter("endpoints_test.go", settings.getModuleName(), writer -> {
251256
ProtocolGenerator.GenerationContext context = contextBuilder.writer(writer).build();
252257
protocolGenerator.generateEndpointResolutionTests(context);
@@ -315,6 +320,19 @@ public Void serviceShape(ServiceShape shape) {
315320
return null;
316321
}
317322

323+
var protocol = protocolGenerator != null
324+
? protocolGenerator.getApplicationProtocol()
325+
: ApplicationProtocol.createDefaultHttpApplicationProtocol();
326+
var context = ProtocolGenerator.GenerationContext.builder()
327+
.protocolName(protocol.getName())
328+
.integrations(integrations)
329+
.model(model)
330+
.service(service)
331+
.settings(settings)
332+
.symbolProvider(symbolProvider)
333+
.delegator(writers)
334+
.build();
335+
318336
// Write API client's package doc for the service.
319337
writers.useFileWriter("doc.go", settings.getModuleName(), (writer) -> {
320338
writer.writePackageDocs(String.format(
@@ -344,6 +362,9 @@ public Void serviceShape(ServiceShape shape) {
344362
protocolGenerator, runtimePlugins).run());
345363
}
346364
});
365+
366+
var clientOptions = new ClientOptions(context, protocol);
367+
writers.useFileWriter("options.go", settings.getModuleName(), clientOptions);
347368
return null;
348369
}
349370

0 commit comments

Comments
 (0)