Skip to content

Commit

Permalink
feat: implement smithy reference arch for identity and auth resolution (
Browse files Browse the repository at this point in the history
  • Loading branch information
lucix-aws authored Nov 15, 2023
1 parent bc219e6 commit 86f5e47
Show file tree
Hide file tree
Showing 57 changed files with 3,129 additions and 1,191 deletions.
3 changes: 3 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package auth defines protocol-agnostic authentication types for smithy
// clients.
package auth
47 changes: 47 additions & 0 deletions auth/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package auth

import (
"context"
"time"

"github.com/aws/smithy-go"
)

// Identity contains information that identifies who the user making the
// request is.
type Identity interface {
Expiration() time.Time
}

// IdentityResolver defines the interface through which an Identity is
// retrieved.
type IdentityResolver interface {
GetIdentity(context.Context, smithy.Properties) (Identity, error)
}

// IdentityResolverOptions defines the interface through which an entity can be
// queried to retrieve an IdentityResolver for a given auth scheme.
type IdentityResolverOptions interface {
GetIdentityResolver(schemeID string) IdentityResolver
}

// AnonymousIdentity is a sentinel to indicate no identity.
type AnonymousIdentity struct{}

var _ Identity = (*AnonymousIdentity)(nil)

// Expiration returns the zero value for time, as anonymous identity never
// expires.
func (*AnonymousIdentity) Expiration() time.Time {
return time.Time{}
}

// AnonymousIdentityResolver returns AnonymousIdentity.
type AnonymousIdentityResolver struct{}

var _ IdentityResolver = (*AnonymousIdentityResolver)(nil)

// GetIdentity returns AnonymousIdentity.
func (*AnonymousIdentityResolver) GetIdentity(_ context.Context, _ smithy.Properties) (Identity, error) {
return &AnonymousIdentity{}, nil
}
25 changes: 25 additions & 0 deletions auth/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package auth

import "github.com/aws/smithy-go"

type (
authOptionsKey struct{}
)

// Option represents a possible authentication method for an operation.
type Option struct {
SchemeID string
IdentityProperties smithy.Properties
SignerProperties smithy.Properties
}

// GetAuthOptions gets auth Options from Properties.
func GetAuthOptions(p *smithy.Properties) ([]*Option, bool) {
v, ok := p.Get(authOptionsKey{}).([]*Option)
return v, ok
}

// SetAuthOptions sets auth Options on Properties.
func SetAuthOptions(p *smithy.Properties, options []*Option) {
p.Set(authOptionsKey{}, options)
}
20 changes: 20 additions & 0 deletions auth/scheme_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package auth

// Anonymous
const (
SchemeIDAnonymous = "smithy.api#noAuth"
)

// HTTP auth schemes
const (
SchemeIDHTTPBasic = "smithy.api#httpBasicAuth"
SchemeIDHTTPDigest = "smithy.api#httpDigestAuth"
SchemeIDHTTPBearer = "smithy.api#httpBearerAuth"
SchemeIDHTTPAPIKey = "smithy.api#httpApiKeyAuth"
)

// AWS auth schemes
const (
SchemeIDSigV4 = "aws.auth#sigv4"
SchemeIDSigV4A = "aws.auth#sigv4a"
)
1 change: 1 addition & 0 deletions codegen/smithy-go-codegen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ extra["moduleName"] = "software.amazon.smithy.go.codegen"

dependencies {
api("software.amazon.smithy:smithy-codegen-core:$smithyVersion")
api("software.amazon.smithy:smithy-aws-traits:$smithyVersion")
implementation("software.amazon.smithy:smithy-waiters:$smithyVersion")
api("com.atlassian.commonmark:commonmark:0.15.2")
api("org.jsoup:jsoup:1.14.1")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package software.amazon.smithy.go.codegen;

import static software.amazon.smithy.go.codegen.GoWriter.goDocTemplate;
import static software.amazon.smithy.go.codegen.GoWriter.goTemplate;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import software.amazon.smithy.go.codegen.auth.AuthSchemeResolverGenerator;
import software.amazon.smithy.go.codegen.integration.AuthSchemeDefinition;
import software.amazon.smithy.go.codegen.integration.ConfigField;
import software.amazon.smithy.go.codegen.integration.ProtocolGenerator;
import software.amazon.smithy.go.codegen.integration.auth.AnonymousDefinition;
import software.amazon.smithy.model.knowledge.ServiceIndex;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.synthetic.NoAuthTrait;
import software.amazon.smithy.utils.MapUtils;

/**
* Implements codegen for service client config.
*/
public class ClientOptions implements GoWriter.Writable {
public static final String NAME = "Options";

private final ProtocolGenerator.GenerationContext context;
private final ApplicationProtocol protocol;

private final List<ConfigField> fields;
private final Map<ShapeId, AuthSchemeDefinition> authSchemes;

public ClientOptions(ProtocolGenerator.GenerationContext context, ApplicationProtocol protocol) {
this.context = context;
this.protocol = protocol;

this.fields = context.getIntegrations().stream()
.flatMap(it -> it.getClientPlugins(context.getModel(), context.getService()).stream())
.flatMap(it -> it.getConfigFields().stream())
.distinct()
.sorted(Comparator.comparing(ConfigField::getName))
.toList();
this.authSchemes = context.getIntegrations().stream()
.flatMap(it -> it.getClientPlugins(context.getModel(), context.getService()).stream())
.flatMap(it -> it.getAuthSchemeDefinitions().entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@Override
public void accept(GoWriter writer) {
writer.write(generate());
}

private GoWriter.Writable generate() {
var apiOptionsDocs = goDocTemplate(
"Set of options to modify how an operation is invoked. These apply to all operations "
+ "invoked for this client. Use functional options on operation call to modify this "
+ "list for per operation behavior."
);
return goTemplate("""
$protocolTypes:W
type $options:L struct {
$apiOptionsDocs:W
APIOptions []func($stack:P) error
$fields:W
$protocolFields:W
}
$copy:W
$getIdentityResolver:W
$helpers:W
""",
MapUtils.of(
"protocolTypes", generateProtocolTypes(),
"apiOptionsDocs", apiOptionsDocs,
"options", NAME,
"stack", SmithyGoTypes.Middleware.Stack,
"fields", GoWriter.ChainWritable.of(fields.stream().map(this::writeField).toList()).compose(),
"protocolFields", generateProtocolFields(),
"copy", generateCopy(),
"getIdentityResolver", generateGetIdentityResolver(),
"helpers", generateHelpers()
));
}

private GoWriter.Writable generateProtocolTypes() {
ensureSupportedProtocol();
return goTemplate("""
type HTTPClient interface {
Do($P) ($P, error)
}
""", GoStdlibTypes.Net.Http.Request, GoStdlibTypes.Net.Http.Response);
}

private GoWriter.Writable writeField(ConfigField field) {
GoWriter.Writable docs = writer -> {
field.getDocumentation().ifPresent(writer::writeDocs);
field.getDeprecated().ifPresent(s -> {
if (field.getDocumentation().isPresent()) {
writer.writeDocs("");
}
writer.writeDocs(String.format("Deprecated: %s", s));
});
};
return goTemplate("""
$W
$L $P
""", docs, field.getName(), field.getType());
}

private GoWriter.Writable generateProtocolFields() {
ensureSupportedProtocol();
return goTemplate("""
$1W
HTTPClient HTTPClient
$2W
AuthSchemeResolver $4L
$3W
AuthSchemes []$5T
""",
goDocTemplate("The HTTP client to invoke API calls with. "
+ "Defaults to client's default HTTP implementation if nil."),
goDocTemplate("The auth scheme resolver which determines how to authenticate for each operation."),
goDocTemplate("The list of auth schemes supported by the client."),
AuthSchemeResolverGenerator.INTERFACE_NAME,
SmithyGoTypes.Transport.Http.AuthScheme);
}

private GoWriter.Writable generateCopy() {
return goTemplate("""
// Copy creates a clone where the APIOptions list is deep copied.
func (o $1L) Copy() $1L {
to := o
to.APIOptions = make([]func($2P) error, len(o.APIOptions))
copy(to.APIOptions, o.APIOptions)
return to
}
""", NAME, SmithyGoTypes.Middleware.Stack);
}

private GoWriter.Writable generateGetIdentityResolver() {
return goTemplate("""
func (o $L) GetIdentityResolver(schemeID string) $T {
$W
$W
return nil
}
""",
NAME,
SmithyGoTypes.Auth.IdentityResolver,
GoWriter.ChainWritable.of(
ServiceIndex.of(context.getModel())
.getEffectiveAuthSchemes(context.getService()).keySet().stream()
.filter(authSchemes::containsKey)
.map(trait -> generateGetIdentityResolverMapping(trait, authSchemes.get(trait)))
.toList()
).compose(false),
generateGetIdentityResolverMapping(NoAuthTrait.ID, new AnonymousDefinition()));
}

private GoWriter.Writable generateGetIdentityResolverMapping(ShapeId schemeId, AuthSchemeDefinition scheme) {
return goTemplate("""
if schemeID == $S {
return $W
}""", schemeId.toString(), scheme.generateOptionsIdentityResolver());
}

private GoWriter.Writable generateHelpers() {
return writer -> {
writer.write("""
$W
func WithAPIOptions(optFns ...func($P) error) func(*Options) {
return func (o *Options) {
o.APIOptions = append(o.APIOptions, optFns...)
}
}
""",
goDocTemplate(
"WithAPIOptions returns a functional option for setting the Client's APIOptions option."
),
SmithyGoTypes.Middleware.Stack);

fields.stream().filter(ConfigField::getWithHelper).filter(ConfigField::isDeprecated)
.forEach(configField -> {
writer.writeDocs(configField.getDeprecated().get());
writeHelper(writer, configField);
});

fields.stream().filter(ConfigField::getWithHelper).filter(Predicate.not(ConfigField::isDeprecated))
.forEach(configField -> {
writer.writeDocs(
String.format(
"With%s returns a functional option for setting the Client's %s option.",
configField.getName(), configField.getName()));
writeHelper(writer, configField);
});
};
}

private void writeHelper(GoWriter writer, ConfigField configField) {
writer.write("""
func With$1L(v $2P) func(*Options) {
return func(o *Options) {
o.$1L = v
}
}
""", configField.getName(), configField.getType());
}

private void ensureSupportedProtocol() {
if (!protocol.isHttpProtocol()) {
throw new UnsupportedOperationException("Protocols other than HTTP are not yet implemented: " + protocol);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ void execute() {
protocolGenerator.generateEndpointResolution(context);
});

writers.useFileWriter("auth.go", settings.getModuleName(), writer -> {
ProtocolGenerator.GenerationContext context = contextBuilder.writer(writer).build();
protocolGenerator.generateAuth(context);
});

writers.useFileWriter("endpoints_test.go", settings.getModuleName(), writer -> {
ProtocolGenerator.GenerationContext context = contextBuilder.writer(writer).build();
protocolGenerator.generateEndpointResolutionTests(context);
Expand Down Expand Up @@ -315,6 +320,19 @@ public Void serviceShape(ServiceShape shape) {
return null;
}

var protocol = protocolGenerator != null
? protocolGenerator.getApplicationProtocol()
: ApplicationProtocol.createDefaultHttpApplicationProtocol();
var context = ProtocolGenerator.GenerationContext.builder()
.protocolName(protocol.getName())
.integrations(integrations)
.model(model)
.service(service)
.settings(settings)
.symbolProvider(symbolProvider)
.delegator(writers)
.build();

// Write API client's package doc for the service.
writers.useFileWriter("doc.go", settings.getModuleName(), (writer) -> {
writer.writePackageDocs(String.format(
Expand Down Expand Up @@ -344,6 +362,9 @@ public Void serviceShape(ServiceShape shape) {
protocolGenerator, runtimePlugins).run());
}
});

var clientOptions = new ClientOptions(context, protocol);
writers.useFileWriter("options.go", settings.getModuleName(), clientOptions);
return null;
}

Expand Down
Loading

0 comments on commit 86f5e47

Please sign in to comment.