Skip to content

Commit

Permalink
refactor!: authentication and identity APIs (#886)
Browse files Browse the repository at this point in the history
* feat: identity API upstream changes (#864)
* track upstream codegen changes (#879)
* track upstream IdentityProvider and Attributes changes (#881)
* add base class for credentials config (#883)

BREAKING CHANGE: `CredentialsProvider` method name and signature changed. `signer` property removed from service client config in favor of `authSchemes` override.
  • Loading branch information
aajtodd authored Apr 10, 2023
1 parent 1710da5 commit 494da0e
Show file tree
Hide file tree
Showing 10 changed files with 61 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ class AwsServiceConfigIntegration : KotlinIntegration {
order = -100
}

// FIXME - this should be registered based on auth scheme in model
// override the credentials provider prop registered by the Sigv4AuthSchemeIntegration, updates the
// documentation and sets a default value for AWS SDK to the default chain.
val CredentialsProviderProp: ConfigProperty = ConfigProperty {
symbol = RuntimeTypes.Auth.Credentials.AwsCredentials.CredentialsProvider
baseClass = RuntimeTypes.Auth.Credentials.AwsCredentials.CredentialsProviderConfig
useNestedBuilderBaseClass()
documentation = """
The AWS credentials provider to use for authenticating requests. If not provided a
[${AwsRuntimeTypes.Config.Credentials.DefaultChainCredentialsProvider}] instance will be used.
Expand All @@ -43,7 +46,7 @@ class AwsServiceConfigIntegration : KotlinIntegration {

propertyType = ConfigPropertyType.Custom(render = { prop, writer ->
writer.write(
"public val #1L: #2T = builder.#1L ?: #3T(httpClientEngine = httpClientEngine, region = region).#4T()",
"override val #1L: #2T = builder.#1L ?: #3T(httpClientEngine = httpClientEngine, region = region).#4T()",
prop.propertyName,
prop.symbol,
AwsRuntimeTypes.Config.Credentials.DefaultChainCredentialsProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes
import software.amazon.smithy.kotlin.codegen.model.buildSymbol
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.kotlin.codegen.model.expectTrait
import software.amazon.smithy.kotlin.codegen.model.knowledge.AwsSignatureVersion4
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointParameterBindingGenerator
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointParametersGenerator
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointProviderGenerator
Expand All @@ -39,7 +40,6 @@ import software.amazon.smithy.kotlin.codegen.rendering.serde.serializerName
import software.amazon.smithy.kotlin.codegen.rendering.util.AbstractConfigGenerator
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigProperty
import software.amazon.smithy.kotlin.codegen.rendering.util.ConfigPropertyType
import software.amazon.smithy.kotlin.codegen.signing.AwsSignatureVersion4
import software.amazon.smithy.kotlin.codegen.utils.dq
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,44 @@ package aws.sdk.kotlin.codegen.customization.s3
import software.amazon.smithy.kotlin.codegen.KotlinSettings
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
import software.amazon.smithy.kotlin.codegen.core.withBlock
import software.amazon.smithy.kotlin.codegen.integration.AuthSchemeHandler
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
import software.amazon.smithy.kotlin.codegen.model.expectShape
import software.amazon.smithy.kotlin.codegen.model.knowledge.AwsSignatureVersion4
import software.amazon.smithy.kotlin.codegen.rendering.auth.SigV4AuthSchemeHandler
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware
import software.amazon.smithy.kotlin.codegen.rendering.protocol.replace
import software.amazon.smithy.kotlin.codegen.signing.AwsSignatureVersion4
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ServiceShape

/**
* Overrides the SigV4 signing middleware config for S3.
* Overrides the SigV4 auth scheme registered by [software.amazon.smithy.kotlin.codegen.rendering.auth.Sigv4AuthSchemeIntegration] for S3.
*/
class S3SigningConfig : KotlinIntegration {

// auth schemes are de-duped by taking the last one registered
override val order: Byte
get() = 127

override fun enabledForService(model: Model, settings: KotlinSettings) =
model.expectShape<ServiceShape>(settings.service).isS3

override fun customizeMiddleware(
ctx: ProtocolGenerator.GenerationContext,
resolved: List<ProtocolMiddleware>,
): List<ProtocolMiddleware> {
val signingServiceName = AwsSignatureVersion4.signingServiceName(ctx.service)

return resolved.replace(newValue = S3SigningMiddleware(signingServiceName)) { middleware ->
middleware.name == RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsHttpSigner.name
}
override fun authSchemes(ctx: ProtocolGenerator.GenerationContext): List<AuthSchemeHandler> {
return listOf(S3AuthSchemeHandler())
}
}

private class S3SigningMiddleware(signingServiceName: String) : AwsSignatureVersion4(signingServiceName) {
override fun renderSigningConfig(op: OperationShape, writer: KotlinWriter) {
super.renderSigningConfig(op, writer)
val sbh = RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSignedBodyHeader
writer.write("signedBodyHeader = #T.X_AMZ_CONTENT_SHA256", sbh)

// https://github.com/awslabs/aws-sdk-kotlin/issues/200
writer.write("useDoubleUriEncode = false")
writer.write("normalizeUriPath = false")
private class S3AuthSchemeHandler : SigV4AuthSchemeHandler() {
override fun instantiateAuthSchemeExpr(ctx: ProtocolGenerator.GenerationContext, writer: KotlinWriter) {
val signingService = AwsSignatureVersion4.signingServiceName(ctx.service)
writer.withBlock("#T(", ")", RuntimeTypes.Auth.HttpAuthAws.SigV4AuthScheme) {
withBlock("#T.Config().apply {", "}", RuntimeTypes.Auth.HttpAuthAws.AwsHttpSigner) {
write("signer = #T", RuntimeTypes.Auth.Signing.AwsSigningStandard.DefaultAwsSigner)
write("service = #S", signingService)
write("signedBodyHeader = #T.X_AMZ_CONTENT_SHA256", RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSignedBodyHeader)
// https://github.com/awslabs/aws-sdk-kotlin/issues/200
writer.write("useDoubleUriEncode = false")
writer.write("normalizeUriPath = false")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import aws.sdk.kotlin.codegen.sdkId
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.model.buildSymbol
import software.amazon.smithy.kotlin.codegen.model.hasIdempotentTokenMember
import software.amazon.smithy.kotlin.codegen.model.knowledge.AwsSignatureVersion4
import software.amazon.smithy.kotlin.codegen.model.namespace
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpBindingResolver
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpProtocolClientGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware
import software.amazon.smithy.kotlin.codegen.signing.AwsSignatureVersion4
import software.amazon.smithy.model.knowledge.OperationIndex
import software.amazon.smithy.model.shapes.OperationShape

Expand Down Expand Up @@ -63,8 +63,6 @@ open class AwsHttpProtocolClientGenerator(
private fun renderMergeServiceDefaults(writer: KotlinWriter) {
// FIXME - we likely need a way to let customizations modify/override this
// FIXME - we also need a way to tie in config properties added via integrations that need to influence the context
writer.addImport(RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes)
writer.addImport(AwsRuntimeTypes.Core.Client.AwsClientOption)
val putIfAbsentSym = buildSymbol { name = "putIfAbsent"; namespace(KotlinDependency.CORE, "util") }
val sdkClientOptionSym = buildSymbol { name = "SdkClientOption"; namespace(KotlinDependency.CORE, "client") }

Expand All @@ -77,15 +75,14 @@ open class AwsHttpProtocolClientGenerator(
write("ctx.#T(#T.Region, config.region)", putIfAbsentSym, AwsRuntimeTypes.Core.Client.AwsClientOption)
write("ctx.#T(#T.ClientName, config.clientName)", putIfAbsentSym, sdkClientOptionSym)
write("ctx.#T(#T.LogMode, config.sdkLogMode)", putIfAbsentSym, sdkClientOptionSym)

// fill in auth/signing attributes
if (AwsSignatureVersion4.isSupportedAuthentication(ctx.model, ctx.service)) {
// default signing context (most of this has been moved to auth schemes but some things like event streams still depend on this)
val signingServiceName = AwsSignatureVersion4.signingServiceName(ctx.service)
write("ctx.#T(#T.SigningService, #S)", putIfAbsentSym, RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes, signingServiceName)
write("ctx.#T(#T.Signer, config.signer)", putIfAbsentSym, RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes)
write("ctx.#T(#T.SigningRegion, config.region)", putIfAbsentSym, RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes)
write("ctx.#T(#T.CredentialsProvider, config.credentialsProvider)", putIfAbsentSym, RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes)
}
write("ctx.#T(#T.SigningRegion, config.region)", putIfAbsentSym, RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes)
write("ctx.#T(#T.CredentialsProvider, config.credentialsProvider)", putIfAbsentSym, RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes)

if (ctx.service.hasIdempotentTokenMember(ctx.model)) {
write("config.idempotencyTokenProvider?.let { ctx[#T.IdempotencyTokenProvider] = it }", sdkClientOptionSym)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,26 @@ private fun String.toAuthSchemeClassName(): String? =
}

private fun renderAuthSchemes(writer: KotlinWriter, authSchemes: Expression, expressionRenderer: ExpressionRenderer) {
writer.withBlock("set(", ")") {
write("#T,", AwsRuntimeTypes.Endpoint.AuthSchemesAttributeKey)
withBlock("listOf(", "),") {
authSchemes.toNode().expectArrayNode().forEach {
val scheme = it.expectObjectNode()
val schemeName = scheme.expectStringMember("name").value
val className = schemeName.toAuthSchemeClassName() ?: return@forEach
writer.writeInline("#T to ", AwsRuntimeTypes.Endpoint.AuthSchemesAttributeKey)
writer.withBlock("listOf(", ")") {
authSchemes.toNode().expectArrayNode().forEach {
val scheme = it.expectObjectNode()
val schemeName = scheme.expectStringMember("name").value
val className = schemeName.toAuthSchemeClassName() ?: return@forEach

withBlock("#T.#L(", "),", AwsRuntimeTypes.Endpoint.AuthScheme, className) {
// we delegate back to the expression visitor for each of these fields because it's possible to
// encounter template strings throughout
withBlock("#T.#L(", "),", AwsRuntimeTypes.Endpoint.AuthScheme, className) {
// we delegate back to the expression visitor for each of these fields because it's possible to
// encounter template strings throughout

writeInline("signingName = ")
renderOrElse(expressionRenderer, scheme.getStringMember("signingName"), "null")
writeInline("signingName = ")
renderOrElse(expressionRenderer, scheme.getStringMember("signingName"), "null")

writeInline("disableDoubleEncoding = ")
renderOrElse(expressionRenderer, scheme.getBooleanMember("disableDoubleEncoding"), "false")
writeInline("disableDoubleEncoding = ")
renderOrElse(expressionRenderer, scheme.getBooleanMember("disableDoubleEncoding"), "false")

when (schemeName) {
"sigv4" -> renderSigV4Fields(writer, scheme, expressionRenderer)
"sigv4a" -> renderSigV4AFields(writer, scheme, expressionRenderer)
}
when (schemeName) {
"sigv4" -> renderSigV4Fields(writer, scheme, expressionRenderer)
"sigv4a" -> renderSigV4AFields(writer, scheme, expressionRenderer)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,24 @@ class EventStreamSerializerGenerator(
val streamShape = ctx.model.expectShape<UnionShape>(streamingMember.target)

writer.write("val stream = input.#L ?: return #T.Empty", streamingMember.defaultName(), RuntimeTypes.Http.HttpBody)
writer.write("val signingConfig = context.#T()", RuntimeTypes.AwsEventStream.newEventStreamSigningConfig)

// initial HTTP request should use an empty body hash since the actual body is the event stream
writer.write("context[#T.HashSpecification] = #T.EmptyBody", RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes, RuntimeTypes.Auth.Signing.AwsSigningCommon.HashSpecification)

// FIXME - we need a signer implementation which usually comes from the auth scheme...for now default to default signer for event streams
writer.write("context[#T.Signer] = #T", RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes, RuntimeTypes.Auth.Signing.AwsSigningStandard.DefaultAwsSigner)
// ensure a deferred is set, signer will complete it when initial request signature is available
writer.write(
"context[#T.RequestSignature] = #T(context.coroutineContext.#T)",
RuntimeTypes.Auth.Signing.AwsSigningCommon.AwsSigningAttributes,
RuntimeTypes.KotlinxCoroutines.CompletableDeferred,
RuntimeTypes.KotlinxCoroutines.job,
)

val encodeFn = encodeEventStreamMessage(ctx, op, streamShape)
writer.withBlock("val messages = stream", "") {
write(".#T(::#T)", RuntimeTypes.KotlinxCoroutines.Flow.map, encodeFn)
write(".#T(context, signingConfig)", RuntimeTypes.AwsEventStream.sign)
write(".#T(context)", RuntimeTypes.AwsEventStream.sign)
write(".#T()", RuntimeTypes.AwsEventStream.encode)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import aws.sdk.kotlin.codegen.AwsRuntimeTypes
import software.amazon.smithy.kotlin.codegen.core.KotlinWriter
import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes
import software.amazon.smithy.kotlin.codegen.model.hasTrait
import software.amazon.smithy.kotlin.codegen.model.knowledge.AwsSignatureVersion4
import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointProviderGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpProtocolUnitTestGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpProtocolUnitTestRequestGenerator
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.kotlin.codegen.signing.AwsSignatureVersion4
import software.amazon.smithy.kotlin.codegen.utils.getOrNull
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.OperationShape
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ aws.sdk.kotlin.codegen.AwsRetryHeaderIntegration
aws.sdk.kotlin.codegen.customization.s3.S3GeneratorSupplier
aws.sdk.kotlin.codegen.GradleGenerator
aws.sdk.kotlin.codegen.AwsServiceConfigIntegration
software.amazon.smithy.kotlin.codegen.signing.AwsSignerIntegration
aws.sdk.kotlin.codegen.customization.s3.S3SigningConfig
aws.sdk.kotlin.codegen.customization.s3.S3ErrorMetadataIntegration
aws.sdk.kotlin.codegen.customization.s3.GetBucketLocationDeserializerIntegration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class AwsServiceConfigIntegrationTest {

val expectedProps = """
override val region: String = requireNotNull(builder.region) { "region is a required configuration property" }
public val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider(httpClientEngine = httpClientEngine, region = region).manage()
override val credentialsProvider: CredentialsProvider = builder.credentialsProvider ?: DefaultChainCredentialsProvider(httpClientEngine = httpClientEngine, region = region).manage()
"""
contents.shouldContainOnlyOnceWithDiff(expectedProps)

Expand All @@ -62,7 +62,7 @@ class AwsServiceConfigIntegrationTest {
* NOTE: The caller is responsible for managing the lifetime of the provider when set. The SDK
* client will not close it when the client is closed.
*/
public var credentialsProvider: CredentialsProvider? = null
override var credentialsProvider: CredentialsProvider? = null
"""
contents.shouldContainOnlyOnceWithDiff(expectedImpl)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ class AwsHttpProtocolClientGeneratorTest {
generator.render(writer)
val contents = writer.toString()
val expected = """
op.execution.retryStrategy = config.retryStrategy
op.execution.retryPolicy = config.retryPolicy
"""
contents.shouldContainOnlyOnceWithDiff(expected)
Expand Down

0 comments on commit 494da0e

Please sign in to comment.