Skip to content

Commit 5ff6681

Browse files
authored
feat: add support for bedrock api key auth (#1642)
* add support for bedrock api key auth * lint * add changelog * disable customization for services not using httpbearer * pr feedbacks * lint * pr feedbacks * use clientName function * style * add comment * style & rename codegen file * move e2e test to commonTest * smithy version bump
1 parent cc837ef commit 5ff6681

File tree

6 files changed

+353
-2
lines changed

6 files changed

+353
-2
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "b6b48ccb-8aa8-40be-bc5a-f5d184e8631e",
3+
"type": "feature",
4+
"description": "Add support for Bearer authentication using a token set in an environment variable for Bedrock services"
5+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.codegen.customization
6+
7+
import aws.sdk.kotlin.codegen.SdkIdTransform
8+
import aws.sdk.kotlin.codegen.ServiceClientCompanionObjectWriter
9+
import aws.sdk.kotlin.codegen.withTransform
10+
import software.amazon.smithy.kotlin.codegen.KotlinSettings
11+
import software.amazon.smithy.kotlin.codegen.core.*
12+
import software.amazon.smithy.kotlin.codegen.integration.AppendingSectionWriter
13+
import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration
14+
import software.amazon.smithy.kotlin.codegen.integration.SectionWriterBinding
15+
import software.amazon.smithy.kotlin.codegen.model.buildSymbol
16+
import software.amazon.smithy.kotlin.codegen.model.expectShape
17+
import software.amazon.smithy.kotlin.codegen.model.hasTrait
18+
import software.amazon.smithy.kotlin.codegen.model.knowledge.AwsSignatureVersion4
19+
import software.amazon.smithy.kotlin.codegen.rendering.ServiceClientGenerator
20+
import software.amazon.smithy.model.Model
21+
import software.amazon.smithy.model.shapes.ServiceShape
22+
import software.amazon.smithy.model.traits.HttpBearerAuthTrait
23+
24+
/**
25+
* Customization that enables sourcing Bearer tokens from an environment variable
26+
*
27+
* When a service-specific environment variable for bearer tokens is present (e.g., AWS_BEARER_TOKEN_BEDROCK),
28+
* this customization configures the auth scheme resolver to prefer the smithy.api#httpBearerAuth scheme
29+
* over other authentication methods. Additionally, it configures a token provider that extracts the bearer token
30+
* from the target environment variable.
31+
*/
32+
class EnvironmentBearerTokenCustomization : KotlinIntegration {
33+
// Currently only services with sigv4 service name 'bedrock' need this customization
34+
private val supportedSigningServiceNames = setOf("bedrock")
35+
36+
override fun enabledForService(model: Model, settings: KotlinSettings): Boolean {
37+
val serviceShape = settings.getService(model)
38+
if (!AwsSignatureVersion4.isSupportedAuthentication(model, serviceShape)) {
39+
return false
40+
}
41+
if (!serviceShape.hasTrait<HttpBearerAuthTrait>()) {
42+
return false
43+
}
44+
45+
return AwsSignatureVersion4.signingServiceName(serviceShape) in supportedSigningServiceNames
46+
}
47+
48+
override fun writeAdditionalFiles(ctx: CodegenContext, delegator: KotlinDelegator) {
49+
val serviceShape = ctx.model.expectShape<ServiceShape>(ctx.settings.service)
50+
val packageName = ctx.settings.pkg.name
51+
52+
delegator.useFileWriter(
53+
"FinalizeBearerTokenConfig.kt",
54+
"$packageName.auth",
55+
) { writer ->
56+
renderEnvironmentBearerTokenConfig(
57+
writer,
58+
ctx,
59+
serviceShape,
60+
)
61+
}
62+
}
63+
64+
private fun renderEnvironmentBearerTokenConfig(
65+
writer: KotlinWriter,
66+
ctx: CodegenContext,
67+
serviceShape: ServiceShape,
68+
) {
69+
val serviceSymbol = ctx.symbolProvider.toSymbol(serviceShape)
70+
val signingServiceName = AwsSignatureVersion4.signingServiceName(serviceShape)
71+
// Transform signing name to environment variable name
72+
val envVarSuffix = signingServiceName.withTransform(SdkIdTransform.UpperSnakeCase)
73+
val envVarName = "AWS_BEARER_TOKEN_$envVarSuffix"
74+
val authSchemeId = RuntimeTypes.Auth.Identity.AuthSchemeId
75+
76+
writer.withBlock(
77+
"internal fun finalizeBearerTokenConfig(builder: #1T.Builder, provider: #2T = #2T.System) {",
78+
"}",
79+
serviceSymbol,
80+
RuntimeTypes.Core.Utils.PlatformProvider,
81+
) {
82+
// The customization do nothing if environment variable is not set
83+
write("if (provider.getenv(#S) == null) { return }", envVarName)
84+
85+
// Configure auth scheme preference if customer hasn't specify one
86+
write("builder.config.authSchemePreference = builder.config.authSchemePreference ?: listOf(#T.HttpBearer)", authSchemeId)
87+
88+
// Promote HttpBearer to first position in auth scheme preference list
89+
withBlock("val filteredSchemes = builder.config.authSchemePreference?.filterNot {", "} ?: emptyList()") {
90+
write("it == #T.HttpBearer", authSchemeId)
91+
}
92+
93+
write("builder.config.authSchemePreference = listOf(#1T.HttpBearer) + filteredSchemes", authSchemeId)
94+
95+
write(
96+
"builder.config.bearerTokenProvider = builder.config.bearerTokenProvider ?: #T(#S, provider)",
97+
RuntimeTypes.Auth.HttpAuth.EnvironmentBearerTokenProvider,
98+
envVarName,
99+
)
100+
}
101+
}
102+
103+
override val sectionWriters: List<SectionWriterBinding>
104+
get() = listOf(
105+
SectionWriterBinding(
106+
ServiceClientCompanionObjectWriter.FinalizeEnvironmentalConfig,
107+
finalizeEnvironmentBearerTokenConfigWriter,
108+
),
109+
)
110+
111+
private val finalizeEnvironmentBearerTokenConfigWriter = AppendingSectionWriter { writer ->
112+
val serviceName = clientName(writer.getContextValue(ServiceClientGenerator.Sections.CompanionObject.SdkId))
113+
114+
val environmentBearerTokenConfig = buildSymbol {
115+
name = "finalizeBearerTokenConfig"
116+
namespace = "aws.sdk.kotlin.services.${serviceName.lowercase()}.auth"
117+
}
118+
119+
writer.write("#T(builder)", environmentBearerTokenConfig)
120+
}
121+
}

codegen/aws-sdk-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestSuccessHttpEngineIntegration
4949
aws.sdk.kotlin.codegen.smoketests.testing.SmokeTestFailHttpEngineIntegration
5050
aws.sdk.kotlin.codegen.customization.AwsQueryModeCustomization
5151
aws.sdk.kotlin.codegen.ModuleDocumentationIntegration
52+
aws.sdk.kotlin.codegen.customization.EnvironmentBearerTokenCustomization
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package aws.sdk.kotlin.codegen.customization
6+
7+
import software.amazon.smithy.kotlin.codegen.test.*
8+
import kotlin.test.Test
9+
import kotlin.test.assertFalse
10+
import kotlin.test.assertTrue
11+
12+
class EnvironmentBearerTokenCustomizationTest {
13+
@Test
14+
fun `test customization enabled for bedrock sigv4 signing name`() {
15+
val bedrockModel = """
16+
namespace com.test
17+
use aws.auth#sigv4
18+
use aws.api#service
19+
use smithy.api#httpBearerAuth
20+
21+
@sigv4(name: "bedrock")
22+
@httpBearerAuth
23+
@service(sdkId: "Bedrock")
24+
service Bedrock {
25+
version: "1.0.0"
26+
}
27+
""".trimIndent().toSmithyModel()
28+
29+
assertTrue {
30+
EnvironmentBearerTokenCustomization()
31+
.enabledForService(bedrockModel, bedrockModel.defaultSettings())
32+
}
33+
}
34+
35+
fun `test customization enabled for bedrock sigv4 signing name with different sdkId`() {
36+
val bedrockRuntimeModel = """
37+
namespace com.test
38+
use aws.auth#sigv4
39+
use aws.api#service
40+
use smithy.api#httpBearerAuth
41+
42+
@sigv4(name: "bedrock")
43+
@httpBearerAuth
44+
@service(sdkId: "Bedrock Runtime")
45+
service BedrockRuntime {
46+
version: "1.0.0"
47+
}
48+
""".trimIndent().toSmithyModel()
49+
50+
assertTrue {
51+
EnvironmentBearerTokenCustomization()
52+
.enabledForService(bedrockRuntimeModel, bedrockRuntimeModel.defaultSettings())
53+
}
54+
}
55+
56+
@Test
57+
fun `test customization not enabled for non-bedrock sigv4 signing name`() {
58+
val nonBedrockModel = """
59+
namespace com.test
60+
use aws.auth#sigv4
61+
use aws.api#service
62+
use smithy.api#httpBearerAuth
63+
64+
@sigv4(name: "s3")
65+
@httpBearerAuth
66+
@service(sdkId: "S3")
67+
service S3 {
68+
version: "1.0.0"
69+
}
70+
""".trimIndent().toSmithyModel()
71+
72+
assertFalse {
73+
EnvironmentBearerTokenCustomization()
74+
.enabledForService(nonBedrockModel, nonBedrockModel.defaultSettings())
75+
}
76+
}
77+
78+
@Test
79+
fun `test customization not enabled for model without sigv4 trait`() {
80+
val noSigV4Model = """
81+
namespace com.test
82+
use aws.api#service
83+
use smithy.api#httpBearerAuth
84+
85+
@service(sdkId: "NoSigV4")
86+
@httpBearerAuth
87+
service NoSigV4 {
88+
version: "1.0.0"
89+
}
90+
""".trimIndent().toSmithyModel()
91+
92+
assertFalse {
93+
EnvironmentBearerTokenCustomization()
94+
.enabledForService(noSigV4Model, noSigV4Model.defaultSettings())
95+
}
96+
}
97+
98+
@Test
99+
fun `test customization not enabled for model without bearer auth trait`() {
100+
val noBearerAuthModel = """
101+
namespace com.test
102+
use aws.auth#sigv4
103+
use aws.api#service
104+
105+
@sigv4(name: "bedrock")
106+
@service(sdkId: "BedrockNoBearerAuth")
107+
service BedrockNoBearerAuth {
108+
version: "1.0.0"
109+
}
110+
""".trimIndent().toSmithyModel()
111+
112+
assertFalse {
113+
EnvironmentBearerTokenCustomization()
114+
.enabledForService(noBearerAuthModel, noBearerAuthModel.defaultSettings())
115+
}
116+
}
117+
}

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ atomicfu-version = "0.25.0"
1212
binary-compatibility-validator-version = "0.16.3"
1313

1414
# smithy-kotlin codegen and runtime are versioned separately
15-
smithy-kotlin-runtime-version = "1.4.22"
16-
smithy-kotlin-codegen-version = "0.34.22"
15+
smithy-kotlin-runtime-version = "1.4.23"
16+
smithy-kotlin-codegen-version = "0.34.23"
1717

1818
# codegen
1919
smithy-version = "1.60.2"
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package aws.sdk.kotlin.services.bedrock
2+
3+
import aws.sdk.kotlin.services.bedrock.auth.finalizeBearerTokenConfig
4+
import aws.smithy.kotlin.runtime.auth.AuthSchemeId
5+
import aws.smithy.kotlin.runtime.collections.Attributes
6+
import aws.smithy.kotlin.runtime.collections.emptyAttributes
7+
import aws.smithy.kotlin.runtime.http.Headers
8+
import aws.smithy.kotlin.runtime.http.HttpBody
9+
import aws.smithy.kotlin.runtime.http.HttpCall
10+
import aws.smithy.kotlin.runtime.http.HttpStatusCode
11+
import aws.smithy.kotlin.runtime.http.auth.BearerToken
12+
import aws.smithy.kotlin.runtime.http.auth.BearerTokenProvider
13+
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
14+
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
15+
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
16+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
17+
import aws.smithy.kotlin.runtime.http.response.HttpResponse
18+
import aws.smithy.kotlin.runtime.io.use
19+
import aws.smithy.kotlin.runtime.operation.ExecutionContext
20+
import aws.smithy.kotlin.runtime.time.Instant
21+
import aws.smithy.kotlin.runtime.util.TestPlatformProvider
22+
import kotlinx.coroutines.test.runTest
23+
import kotlin.test.assertEquals
24+
import kotlin.test.assertNotNull
25+
26+
class BedrockEnvironmentBearerTokenTest {
27+
private fun mockHttpClient(handler: (HttpRequest) -> HttpResponse): HttpClientEngine {
28+
return object : HttpClientEngineBase("test engine") {
29+
override val config: HttpClientEngineConfig = HttpClientEngineConfig.Default
30+
31+
override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall {
32+
val response = handler(request)
33+
return HttpCall(request, response, Instant.now(), Instant.now())
34+
}
35+
}
36+
}
37+
38+
private val mockPlatformProvider = TestPlatformProvider(
39+
env = mapOf("AWS_BEARER_TOKEN_BEDROCK" to "bedrock-token"),
40+
)
41+
42+
fun testAuthSchemePreferenceConfigured() = runTest {
43+
val builder = BedrockClient.Builder()
44+
val expectedAuthSchemePreference = listOf(AuthSchemeId.HttpBearer)
45+
finalizeBearerTokenConfig(builder, mockPlatformProvider)
46+
assertEquals(expectedAuthSchemePreference, builder.config.authSchemePreference)
47+
}
48+
49+
fun testBearerAuthSchemePromotedToFirst() = runTest {
50+
val builder = BedrockClient.Builder()
51+
builder.config.authSchemePreference = listOf(AuthSchemeId.AwsSigV4)
52+
finalizeBearerTokenConfig(builder, mockPlatformProvider)
53+
val expectedAuthSchemePreference = listOf(AuthSchemeId.HttpBearer, AuthSchemeId.AwsSigV4)
54+
assertEquals(expectedAuthSchemePreference, builder.config.authSchemePreference)
55+
56+
builder.config.authSchemePreference = listOf(AuthSchemeId.AwsSigV4, AuthSchemeId.HttpBearer)
57+
assertEquals(expectedAuthSchemePreference, builder.config.authSchemePreference)
58+
}
59+
60+
fun testBearerTokenProviderConfigured() = runTest {
61+
val builder = BedrockClient.Builder()
62+
finalizeBearerTokenConfig(builder, mockPlatformProvider)
63+
assertNotNull(builder.config.bearerTokenProvider)
64+
val token = builder.config.bearerTokenProvider!!.resolve()
65+
assertNotNull(token)
66+
assertEquals("bedrock-token", token.token)
67+
}
68+
69+
fun testExplicitProviderTakesPrecedence() = runTest {
70+
val builder = BedrockClient.Builder()
71+
builder.config.bearerTokenProvider = object : BearerTokenProvider {
72+
override suspend fun resolve(attributes: Attributes): BearerToken = object : BearerToken {
73+
override val token: String = "different-bedrock-token"
74+
override val attributes: Attributes = emptyAttributes()
75+
override val expiration: Instant? = null
76+
}
77+
}
78+
finalizeBearerTokenConfig(builder, mockPlatformProvider)
79+
assertNotNull(builder.config.bearerTokenProvider)
80+
val token = builder.config.bearerTokenProvider!!.resolve()
81+
assertNotNull(token)
82+
assertEquals("different-bedrock-token", token.token)
83+
}
84+
85+
fun testBearerTokenProviderFunctionality() = runTest {
86+
var capturedAuthHeader: String? = null
87+
88+
BedrockClient.fromEnvironment {
89+
region = "us-west-2"
90+
httpClient = mockHttpClient { request ->
91+
// Capture the Authorization header
92+
capturedAuthHeader = request.headers["Authorization"]
93+
HttpResponse(
94+
status = HttpStatusCode.OK,
95+
headers = Headers.Empty,
96+
body = HttpBody.Empty,
97+
)
98+
}
99+
}.use { client ->
100+
// Make an api call to capture Authorization header
101+
client.listFoundationModels()
102+
103+
assertNotNull(capturedAuthHeader)
104+
assertEquals("Bearer bedrock-token", capturedAuthHeader)
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)