-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement smithy reference arch for identity and auth resolution (
#469)
- Loading branch information
Showing
57 changed files
with
3,129 additions
and
1,191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
238 changes: 238 additions & 0 deletions
238
codegen/smithy-go-codegen/src/main/java/software/amazon/smithy/go/codegen/ClientOptions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.