Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
753bbfd
feat(codegen): support for api key auth trait
eduardomourar Jan 1, 2023
be84074
chore: update to new codegen decorator interface
eduardomourar Jan 2, 2023
9aff747
chore: include basic test
eduardomourar Jan 2, 2023
93b99c8
chore: set api key into rest xml extras model
eduardomourar Jan 2, 2023
e3ba4c2
chore: update test
eduardomourar Jan 2, 2023
a7a9658
chore: refactor api key definition map
eduardomourar Jan 2, 2023
95c12df
feat(codegen): add api key decorator by default
eduardomourar Jan 2, 2023
159b494
Merge branch 'main' into feat/codegen-api-key
eduardomourar Feb 14, 2023
b6c9078
chore: add smithy-http-auth to runtime type
eduardomourar Feb 14, 2023
dfbb487
chore: reference new smithy-http-auth crate
eduardomourar Feb 14, 2023
e6064b1
Merge branch 'main' into feat/codegen-api-key
jdisanti Feb 15, 2023
a9babbf
Update codegen-client/src/main/kotlin/software/amazon/smithy/rust/cod…
eduardomourar Feb 15, 2023
215e52c
Update codegen-client/src/main/kotlin/software/amazon/smithy/rust/cod…
eduardomourar Feb 15, 2023
b11c179
Update codegen-client/src/main/kotlin/software/amazon/smithy/rust/cod…
eduardomourar Feb 15, 2023
f03aa64
Revert "chore: set api key into rest xml extras model"
eduardomourar Feb 15, 2023
04cfa8c
chore: moved api key re-export to extras customization
eduardomourar Feb 15, 2023
7b2bc4c
Merge branch 'main' into feat/codegen-api-key
eduardomourar Feb 15, 2023
2a6a780
chore: include test for auth in query and header
eduardomourar Feb 15, 2023
5f73af7
chore: fix linting
eduardomourar Feb 15, 2023
d413e17
Update codegen-client/src/main/kotlin/software/amazon/smithy/rust/cod…
eduardomourar Feb 15, 2023
bb7b4fa
Update codegen-client/src/main/kotlin/software/amazon/smithy/rust/cod…
eduardomourar Feb 15, 2023
b159562
Update codegen-client/src/main/kotlin/software/amazon/smithy/rust/cod…
eduardomourar Feb 15, 2023
039db7b
Update codegen-client/src/test/kotlin/software/amazon/smithy/rust/cod…
eduardomourar Feb 15, 2023
eeff428
Update codegen-client/src/test/kotlin/software/amazon/smithy/rust/cod…
eduardomourar Feb 15, 2023
8a49e2b
chore: add doc hidden to re-export
eduardomourar Feb 15, 2023
5a8c1c1
Merge branch 'main' into feat/codegen-api-key
eduardomourar Feb 15, 2023
d565aca
chore: ensure extras are added only if it applies
eduardomourar Feb 15, 2023
ce2485d
Revert "chore: add doc hidden to re-export"
eduardomourar Feb 15, 2023
e9e4db9
Merge branch 'main' into feat/codegen-api-key
eduardomourar Feb 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import software.amazon.smithy.build.PluginContext
import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.rust.codegen.client.smithy.customizations.ApiKeyAuthDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customizations.ClientCustomizations
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
import software.amazon.smithy.rust.codegen.client.smithy.customize.CombinedClientCodegenDecorator
Expand Down Expand Up @@ -58,6 +59,7 @@ class RustClientCodegenPlugin : ClientDecoratableBuildPlugin() {
FluentClientDecorator(),
EndpointsDecorator(),
NoOpEventStreamSigningDecorator(),
ApiKeyAuthDecorator(),
*decorator,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.client.smithy.customizations

import software.amazon.smithy.model.knowledge.ServiceIndex
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.traits.HttpApiKeyAuthTrait
import software.amazon.smithy.model.traits.OptionalAuthTrait
import software.amazon.smithy.model.traits.Trait
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.client.smithy.ClientRustModule
import software.amazon.smithy.rust.codegen.client.smithy.customize.ClientCodegenDecorator
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ConfigCustomization
import software.amazon.smithy.rust.codegen.client.smithy.generators.config.ServiceConfig
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
import software.amazon.smithy.rust.codegen.core.rustlang.rust
import software.amazon.smithy.rust.codegen.core.rustlang.rustBlock
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.core.rustlang.writable
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.core.smithy.RustCrate
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationCustomization
import software.amazon.smithy.rust.codegen.core.smithy.customize.OperationSection
import software.amazon.smithy.rust.codegen.core.util.expectTrait
import software.amazon.smithy.rust.codegen.core.util.letIf

/**
* Inserts a ApiKeyAuth configuration into the operation
*/
class ApiKeyAuthDecorator : ClientCodegenDecorator {
override val name: String = "ApiKeyAuth"
override val order: Byte = 10

private fun applies(codegenContext: ClientCodegenContext) =
isSupportedApiKeyAuth(codegenContext)

override fun configCustomizations(
codegenContext: ClientCodegenContext,
baseCustomizations: List<ConfigCustomization>,
): List<ConfigCustomization> {
return baseCustomizations.letIf(applies(codegenContext)) { customizations ->
customizations + ApiKeyConfigCustomization(codegenContext.runtimeConfig)
}
}

override fun operationCustomizations(
codegenContext: ClientCodegenContext,
operation: OperationShape,
baseCustomizations: List<OperationCustomization>,
): List<OperationCustomization> {
if (applies(codegenContext) && hasApiKeyAuthScheme(codegenContext, operation)) {
val service = codegenContext.serviceShape
val authDefinition: HttpApiKeyAuthTrait = service.expectTrait(HttpApiKeyAuthTrait::class.java)
return baseCustomizations + ApiKeyOperationCustomization(codegenContext.runtimeConfig, authDefinition)
}
return baseCustomizations
}

override fun extras(codegenContext: ClientCodegenContext, rustCrate: RustCrate) {
if (applies(codegenContext)) {
rustCrate.withModule(ClientRustModule.Config) {
rust("pub use #T;", apiKey(codegenContext.runtimeConfig))
}
}
}
}

/**
* Returns if the service supports the httpApiKeyAuth trait.
*
* @param codegenContext Codegen context that includes the model and service shape
* @return if the httpApiKeyAuth trait is used by the service
*/
private fun isSupportedApiKeyAuth(codegenContext: ClientCodegenContext): Boolean {
return ServiceIndex.of(codegenContext.model).getAuthSchemes(codegenContext.serviceShape).containsKey(HttpApiKeyAuthTrait.ID)
}

/**
* Returns if the service and operation have the httpApiKeyAuthTrait.
*
* @param codegenContext codegen context that includes the model and service shape
* @param operation operation shape
* @return if the service and operation have the httpApiKeyAuthTrait
*/
private fun hasApiKeyAuthScheme(codegenContext: ClientCodegenContext, operation: OperationShape): Boolean {
val auth: Map<ShapeId, Trait> = ServiceIndex.of(codegenContext.model).getEffectiveAuthSchemes(codegenContext.serviceShape.getId(), operation.getId())
return auth.containsKey(HttpApiKeyAuthTrait.ID) && !operation.hasTrait(OptionalAuthTrait.ID)
}

private class ApiKeyOperationCustomization(private val runtimeConfig: RuntimeConfig, private val authDefinition: HttpApiKeyAuthTrait) : OperationCustomization() {
override fun section(section: OperationSection): Writable = when (section) {
is OperationSection.MutateRequest -> writable {
rustBlock("if let Some(api_key_config) = ${section.config}.api_key()") {
rust(
"""
${section.request}.properties_mut().insert(api_key_config.clone());
let api_key = api_key_config.api_key();
""",
)
val definitionName = authDefinition.getName()
if (authDefinition.getIn() == HttpApiKeyAuthTrait.Location.QUERY) {
rustTemplate(
"""
let auth_definition = #{http_auth_definition}::query(
"$definitionName".to_owned(),
);
let name = auth_definition.name();
let mut query = #{query_writer}::new(${section.request}.http().uri());
query.insert(name, api_key);
*${section.request}.http_mut().uri_mut() = query.build_uri();
""",
"http_auth_definition" to
RuntimeType.smithyHttpAuth(runtimeConfig).resolve("definition::HttpAuthDefinition"),
"query_writer" to RuntimeType.smithyHttp(runtimeConfig).resolve("query_writer::QueryWriter"),
)
} else {
val definitionScheme: String = authDefinition.getScheme()
.map { scheme ->
"Some(\"" + scheme + "\".to_owned())"
}
.orElse("None")
rustTemplate(
"""
let auth_definition = #{http_auth_definition}::header(
"$definitionName".to_owned(),
$definitionScheme,
);
let name = auth_definition.name();
let value = match auth_definition.scheme() {
Some(value) => format!("{value} {api_key}"),
None => api_key.to_owned(),
};
${section.request}
.http_mut()
.headers_mut()
.insert(
#{http_header}::HeaderName::from_bytes(name.as_bytes()).expect("valid header name for api key auth"),
#{http_header}::HeaderValue::from_bytes(value.as_bytes()).expect("valid header value for api key auth")
);
""",
"http_auth_definition" to
RuntimeType.smithyHttpAuth(runtimeConfig).resolve("definition::HttpAuthDefinition"),
"http_header" to RuntimeType.Http.resolve("header"),
)
}
}
}
else -> emptySection
}
}

private class ApiKeyConfigCustomization(runtimeConfig: RuntimeConfig) : ConfigCustomization() {
private val codegenScope = arrayOf(
"ApiKey" to apiKey(runtimeConfig),
)

override fun section(section: ServiceConfig): Writable =
when (section) {
is ServiceConfig.BuilderStruct -> writable {
rustTemplate("api_key: Option<#{ApiKey}>,", *codegenScope)
}
is ServiceConfig.BuilderImpl -> writable {
rustTemplate(
"""
/// Sets the API key that will be used by the client.
pub fn api_key(mut self, api_key: #{ApiKey}) -> Self {
self.set_api_key(Some(api_key));
self
}

/// Sets the API key that will be used by the client.
pub fn set_api_key(&mut self, api_key: Option<#{ApiKey}>) -> &mut Self {
self.api_key = api_key;
self
}
""",
*codegenScope,
)
}
is ServiceConfig.BuilderBuild -> writable {
rust("api_key: self.api_key,")
}
is ServiceConfig.ConfigStruct -> writable {
rustTemplate("api_key: Option<#{ApiKey}>,", *codegenScope)
}
is ServiceConfig.ConfigImpl -> writable {
rustTemplate(
"""
/// Returns API key used by the client, if it was provided.
pub fn api_key(&self) -> Option<&#{ApiKey}> {
self.api_key.as_ref()
}
""",
*codegenScope,
)
}
else -> emptySection
}
}

private fun apiKey(runtimeConfig: RuntimeConfig) = RuntimeType.smithyHttpAuth(runtimeConfig).resolve("api_key::AuthApiKey")
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.rust.codegen.client.customizations

import org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.client.testutil.clientIntegrationTest
import software.amazon.smithy.rust.codegen.core.rustlang.Attribute
import software.amazon.smithy.rust.codegen.core.rustlang.rust
import software.amazon.smithy.rust.codegen.core.testutil.IntegrationTestParams
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.core.testutil.integrationTest
import software.amazon.smithy.rust.codegen.core.testutil.runWithWarnings

internal class ApiKeyAuthDecoratorTest {
private val modelQuery = """
namespace test

use aws.api#service
use aws.protocols#restJson1

@service(sdkId: "Test Api Key Auth")
@restJson1
@httpApiKeyAuth(name: "api_key", in: "query")
@auth([httpApiKeyAuth])
service TestService {
version: "2023-01-01",
operations: [SomeOperation]
}

structure SomeOutput {
someAttribute: Long,
someVal: String
}

@http(uri: "/SomeOperation", method: "GET")
operation SomeOperation {
output: SomeOutput
}
""".asSmithyModel()

@Test
fun `set an api key in query parameter`() {
val testDir = clientIntegrationTest(
modelQuery,
// just run integration tests
IntegrationTestParams(command = { "cargo test --test *".runWithWarnings(it) }),
) { clientCodegenContext, rustCrate ->
rustCrate.integrationTest("api_key_present_in_property_bag") {
val moduleName = clientCodegenContext.moduleUseName()
Attribute.TokioTest.render(this)
rust(
"""
async fn api_key_present_in_property_bag() {
use aws_smithy_http_auth::api_key::AuthApiKey;
let api_key_value = "some-api-key";
let conf = $moduleName::Config::builder()
.api_key(AuthApiKey::new(api_key_value))
.build();
let operation = $moduleName::operation::SomeOperation::builder()
.build()
.expect("input is valid")
.make_operation(&conf)
.await
.expect("valid operation");
let props = operation.properties();
let api_key_config = props.get::<AuthApiKey>().expect("api key in the bag");
assert_eq!(
api_key_config,
&AuthApiKey::new(api_key_value),
);
}
""",
)
}

rustCrate.integrationTest("api_key_auth_is_set_in_query") {
val moduleName = clientCodegenContext.moduleUseName()
Attribute.TokioTest.render(this)
rust(
"""
async fn api_key_auth_is_set_in_query() {
use aws_smithy_http_auth::api_key::AuthApiKey;
let api_key_value = "some-api-key";
let conf = $moduleName::Config::builder()
.api_key(AuthApiKey::new(api_key_value))
.build();
let operation = $moduleName::operation::SomeOperation::builder()
.build()
.expect("input is valid")
.make_operation(&conf)
.await
.expect("valid operation");
assert_eq!(
operation.request().uri().query(),
Some("api_key=some-api-key"),
);
}
""",
)
}
}
"cargo clippy".runWithWarnings(testDir)
}

private val modelHeader = """
namespace test

use aws.api#service
use aws.protocols#restJson1

@service(sdkId: "Test Api Key Auth")
@restJson1
@httpApiKeyAuth(name: "authorization", in: "header", scheme: "ApiKey")
@auth([httpApiKeyAuth])
service TestService {
version: "2023-01-01",
operations: [SomeOperation]
}

structure SomeOutput {
someAttribute: Long,
someVal: String
}

@http(uri: "/SomeOperation", method: "GET")
operation SomeOperation {
output: SomeOutput
}
""".asSmithyModel()

@Test
fun `set an api key in http header`() {
val testDir = clientIntegrationTest(
modelHeader,
// just run integration tests
IntegrationTestParams(command = { "cargo test --test *".runWithWarnings(it) }),
) { clientCodegenContext, rustCrate ->
rustCrate.integrationTest("api_key_auth_is_set_in_http_header") {
val moduleName = clientCodegenContext.moduleUseName()
Attribute.TokioTest.render(this)
rust(
"""
async fn api_key_auth_is_set_in_http_header() {
use aws_smithy_http_auth::api_key::AuthApiKey;
let api_key_value = "some-api-key";
let conf = $moduleName::Config::builder()
.api_key(AuthApiKey::new(api_key_value))
.build();
let operation = $moduleName::operation::SomeOperation::builder()
.build()
.expect("input is valid")
.make_operation(&conf)
.await
.expect("valid operation");
let props = operation.properties();
let api_key_config = props.get::<AuthApiKey>().expect("api key in the bag");
assert_eq!(
api_key_config,
&AuthApiKey::new(api_key_value),
);
assert_eq!(
operation.request().headers().contains_key("authorization"),
true,
);
}
""",
)
}
}
"cargo clippy".runWithWarnings(testDir)
}
}
Loading