Skip to content

Commit 1455887

Browse files
authored
feat: Support auth scheme preference list (#934)
1 parent b477e3e commit 1455887

File tree

11 files changed

+267
-7
lines changed

11 files changed

+267
-7
lines changed

Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ let package = Package(
270270
name: "SmithyHTTPAuthTests",
271271
dependencies: ["SmithyHTTPAuth", "SmithyHTTPAPI", "Smithy", "SmithyIdentity", "ClientRuntime"]
272272
),
273+
.testTarget(
274+
name: "SmithyHTTPAuthAPITests",
275+
dependencies: ["SmithyHTTPAuthAPI", "Smithy", "ClientRuntime"]
276+
),
273277
.testTarget(
274278
name: "SmithyJSONTests",
275279
dependencies: ["SmithyJSON", "ClientRuntime", "SmithyTestUtil"]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright Amazon.com Inc. or its affiliates.
2+
// All Rights Reserved.
3+
//
4+
// SPDX-License-Identifier: Apache-2.0
5+
//
6+
7+
/// Reprioritize a resolved list of auth options based on a user's preference list
8+
/// - Parameters:
9+
/// - authSchemePreference: List of preferred auth scheme IDs
10+
/// - authOptions: List of auth options to prioritize
11+
/// - Returns: A new list of auth options with preferred options first, followed by non-preferred options
12+
public extension AuthSchemeResolver {
13+
func reprioritizeAuthOptions(authSchemePreference: [String]?, authOptions: [AuthOption]) -> [AuthOption] {
14+
// Add preferred candidates first
15+
let preferredAuthOptions: [AuthOption]
16+
17+
// For comparison, only use the scheme ID with the namespace prefix trimmed.
18+
if let authSchemePreference {
19+
preferredAuthOptions = authSchemePreference.compactMap { preferredSchemeID in
20+
let preferredSchemeName = normalizedSchemeName(preferredSchemeID)
21+
return authOptions.first { option in
22+
normalizedSchemeName(option.schemeID) == preferredSchemeName
23+
}
24+
}
25+
} else {
26+
preferredAuthOptions = []
27+
}
28+
29+
// Add any remaining candidates that weren't in the preference list
30+
let nonPreferredAuthOptions = authOptions.filter { option in
31+
!preferredAuthOptions.contains { preferred in
32+
normalizedSchemeName(preferred.schemeID) == normalizedSchemeName(option.schemeID)
33+
}
34+
}
35+
36+
return preferredAuthOptions + nonPreferredAuthOptions
37+
}
38+
39+
// Trim namespace prefix from scheme ID
40+
// Ex. aws.auth#sigv4 -> sigv4
41+
func normalizedSchemeName(_ schemeID: String) -> String {
42+
return schemeID.split(separator: "#").last.map(String.init) ?? schemeID
43+
}
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import struct Smithy.Attributes
9+
import struct Smithy.AttributeKey
10+
import class Smithy.Context
11+
import class Smithy.ContextBuilder
12+
13+
extension Context {
14+
/// Gets the auth scheme preference list from the context.
15+
/// - Returns: An array of auth scheme IDs in priority order, or nil if not set.
16+
public func getAuthSchemePreference() -> [String]? {
17+
get(key: authSchemePreferenceKey)
18+
}
19+
}
20+
21+
extension ContextBuilder {
22+
/// Removes the auth scheme preference from the context.
23+
/// - Returns: Self for method chaining.
24+
@discardableResult
25+
public func removeAuthSchemePreference() -> Self {
26+
attributes.remove(key: authSchemePreferenceKey)
27+
return self
28+
}
29+
30+
/// Sets the auth scheme priority from a preference list of IDs.
31+
@discardableResult
32+
public func withAuthSchemePreference(value: [String]?) -> Self {
33+
if value?.isEmpty ?? true {
34+
// If value in empty array, remove the attributes
35+
attributes.remove(key: authSchemePreferenceKey)
36+
} else {
37+
attributes.set(key: authSchemePreferenceKey, value: value)
38+
}
39+
return self
40+
}
41+
}
42+
43+
// Private attribute keys
44+
private let authSchemePreferenceKey = AttributeKey<[String]>(name: "AuthSchemePreference")
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
@testable import SmithyHTTPAuthAPI
9+
import class Smithy.Context
10+
import XCTest
11+
12+
/// A dummy resolver to exercise the AuthSchemeResolver extension
13+
private struct DummyAuthSchemeResolver: AuthSchemeResolver {
14+
func resolveAuthScheme(params: AuthSchemeResolverParameters) throws -> [AuthOption] {
15+
// Not used in reprioritizeAuthOptions tests
16+
return []
17+
}
18+
19+
func constructParameters(context: Context) throws -> AuthSchemeResolverParameters {
20+
// Not used in reprioritizeAuthOptions tests
21+
fatalError("constructParameters() is not implemented for tests")
22+
}
23+
}
24+
25+
final class AuthSchemeResolverTests: XCTestCase {
26+
private var resolver: DummyAuthSchemeResolver!
27+
28+
override func setUp() {
29+
super.setUp()
30+
resolver = DummyAuthSchemeResolver()
31+
}
32+
33+
override func tearDown() {
34+
resolver = nil
35+
super.tearDown()
36+
}
37+
38+
// Manual Auth Schemes Configuration Tests
39+
40+
/// Row 1: Supported Auth contains sigv4 and sigv4a, service trait contains sigv4 and sigv4a
41+
func testManualConfigRow1() {
42+
let options = [
43+
AuthOption(schemeID: "aws.auth#sigv4"),
44+
AuthOption(schemeID: "aws.auth#sigv4a")
45+
]
46+
47+
// No preference list - should maintain original order
48+
let resultNoPreference = resolver.reprioritizeAuthOptions(authSchemePreference: nil, authOptions: options)
49+
XCTAssertEqual(resultNoPreference.map { $0.schemeID }, ["aws.auth#sigv4", "aws.auth#sigv4a"])
50+
51+
// Empty preference list - should maintain original order
52+
let resultEmptyPreference = resolver.reprioritizeAuthOptions(authSchemePreference: [], authOptions: options)
53+
XCTAssertEqual(resultEmptyPreference.map { $0.schemeID }, ["aws.auth#sigv4", "aws.auth#sigv4a"])
54+
55+
// Resolved auth should be sigv4
56+
let preference = ["sigv4"]
57+
let result = resolver.reprioritizeAuthOptions(authSchemePreference: preference, authOptions: options)
58+
XCTAssertEqual(result.first?.schemeID, "aws.auth#sigv4")
59+
}
60+
61+
/// Row 2: Service trait has sigv4, sigv4a; preference list has sigv4a
62+
func testManualConfigRow2() {
63+
let options = [
64+
AuthOption(schemeID: "aws.auth#sigv4"),
65+
AuthOption(schemeID: "aws.auth#sigv4a")
66+
]
67+
let preference = ["sigv4a"]
68+
let result = resolver.reprioritizeAuthOptions(authSchemePreference: preference, authOptions: options)
69+
XCTAssertEqual(result.first?.schemeID, "aws.auth#sigv4a")
70+
}
71+
72+
/// Row 3: Service trait has sigv4, sigv4a; preference list has sigv4a, sigv4
73+
func testManualConfigRow3() {
74+
let options = [
75+
AuthOption(schemeID: "aws.auth#sigv4"),
76+
AuthOption(schemeID: "aws.auth#sigv4a")
77+
]
78+
let preference = ["sigv4a", "sigv4"]
79+
let result = resolver.reprioritizeAuthOptions(authSchemePreference: preference, authOptions: options)
80+
let expectedOrder = ["aws.auth#sigv4a", "aws.auth#sigv4"]
81+
XCTAssertEqual(result.map { $0.schemeID }, expectedOrder)
82+
}
83+
84+
/// Row 4: Service trait has only sigv4; preference list has sigv4a
85+
func testManualConfigRow4() {
86+
let options = [
87+
AuthOption(schemeID: "aws.auth#sigv4")
88+
]
89+
let preference = ["sigv4a"]
90+
let result = resolver.reprioritizeAuthOptions(authSchemePreference: preference, authOptions: options)
91+
// Since sigv4a is not available, should return sigv4
92+
XCTAssertEqual(result.map { $0.schemeID }, ["aws.auth#sigv4"])
93+
}
94+
95+
/// Row 5: Service trait has sigv4, sigv4a; operation trait has sigv4
96+
func testManualConfigRow5() {
97+
// When operation trait specifies sigv4, only sigv4 should be available
98+
let options = [
99+
AuthOption(schemeID: "aws.auth#sigv4")
100+
]
101+
let preference = ["sigv4a"]
102+
let result = resolver.reprioritizeAuthOptions(authSchemePreference: preference, authOptions: options)
103+
XCTAssertEqual(result.map { $0.schemeID }, ["aws.auth#sigv4"])
104+
}
105+
106+
/// Row 6: Service trait has sigv4, sigv4a; preference list has sigv4a
107+
func testManualConfigRow6() {
108+
let options = [
109+
AuthOption(schemeID: "aws.auth#sigv4"),
110+
AuthOption(schemeID: "aws.auth#sigv4a")
111+
]
112+
let preference = ["sigv4a"]
113+
let result = resolver.reprioritizeAuthOptions(authSchemePreference: preference, authOptions: options)
114+
XCTAssertEqual(result.first?.schemeID, "aws.auth#sigv4a")
115+
}
116+
117+
/// Row 7: Service trait has sigv4, sigv4a; preference list has sigv3
118+
func testManualConfigRow7() {
119+
let options = [
120+
AuthOption(schemeID: "aws.auth#sigv4"),
121+
AuthOption(schemeID: "aws.auth#sigv4a")
122+
]
123+
let preference = ["sigv3"]
124+
let result = resolver.reprioritizeAuthOptions(authSchemePreference: preference, authOptions: options)
125+
// Since sigv3 is not available, should maintain original order
126+
XCTAssertEqual(result.map { $0.schemeID }, ["aws.auth#sigv4", "aws.auth#sigv4a"])
127+
}
128+
}

smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/AuthSchemeResolverGenerator.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ class AuthSchemeResolverGenerator {
137137
defaultResolverName,
138138
serviceSpecificAuthResolverProtocol,
139139
) {
140+
write("")
141+
renderInit(writer)
140142
write("")
141143
renderResolveAuthSchemeMethod(serviceIndex, ctx, writer)
142144
write("")
@@ -149,6 +151,19 @@ class AuthSchemeResolverGenerator {
149151
}
150152
}
151153

154+
private fun renderInit(writer: SwiftWriter) {
155+
writer.apply {
156+
write("public let authSchemePreference: [String]")
157+
write("")
158+
openBlock(
159+
"public init(authSchemePreference: [String] = []) {",
160+
"}",
161+
) {
162+
write("self.authSchemePreference = authSchemePreference")
163+
}
164+
}
165+
}
166+
152167
private fun renderResolveAuthSchemeMethod(
153168
serviceIndex: ServiceIndex,
154169
ctx: ProtocolGenerator.GenerationContext,
@@ -175,8 +190,8 @@ class AuthSchemeResolverGenerator {
175190
}
176191
// Render switch block
177192
renderSwitchBlock(serviceIndex, ctx, writer)
178-
// Return result
179-
write("return validAuthOptions")
193+
// Call reprioritizeAuthOptions and return result
194+
write("return self.reprioritizeAuthOptions(authSchemePreference: authSchemePreference, authOptions: validAuthOptions)")
180195
}
181196
}
182197
}

smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/config/DefaultHttpClientConfiguration.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import software.amazon.smithy.swift.codegen.swiftmodules.ClientRuntimeTypes
1616
import software.amazon.smithy.swift.codegen.swiftmodules.SmithyHTTPAPITypes
1717
import software.amazon.smithy.swift.codegen.swiftmodules.SmithyHTTPAuthAPITypes
1818
import software.amazon.smithy.swift.codegen.swiftmodules.SmithyIdentityTypes
19+
import software.amazon.smithy.swift.codegen.swiftmodules.SwiftTypes
1920
import software.amazon.smithy.swift.codegen.utils.AuthUtils
2021

2122
class DefaultHttpClientConfiguration : ClientConfiguration {
@@ -41,6 +42,7 @@ class DefaultHttpClientConfiguration : ClientConfiguration {
4142
{ it.format("\$N.defaultHttpClientConfiguration", ClientRuntimeTypes.Core.ClientConfigurationDefaults) },
4243
),
4344
ConfigProperty("authSchemes", SmithyHTTPAuthAPITypes.AuthSchemes.toOptional(), AuthUtils(ctx).authSchemesDefaultProvider),
45+
ConfigProperty("authSchemePreference", SwiftTypes.StringList.toOptional(), { "nil" }),
4446
ConfigProperty(
4547
"authSchemeResolver",
4648
SmithyHTTPAuthAPITypes.AuthSchemeResolver,

smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/middleware/MiddlewareExecutionGenerator.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class MiddlewareExecutionGenerator(
9898
writer.write(" .withLogger(value: config.logger)")
9999
writer.write(" .withPartitionID(value: config.partitionID)")
100100
writer.write(" .withAuthSchemes(value: config.authSchemes ?? [])")
101+
writer.write(" .withAuthSchemePreference(value: config.authSchemePreference)")
101102
writer.write(" .withAuthSchemeResolver(value: config.authSchemeResolver)")
102103
writer.write(" .withUnsignedPayloadTrait(value: \$L)", op.hasTrait<UnsignedPayloadTrait>())
103104
writer.write(" .withSocketTimeout(value: config.httpClientConfiguration.socketTimeout)")

smithy-swift-codegen/src/main/kotlin/software/amazon/smithy/swift/codegen/swiftmodules/SwiftTypes.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ import software.amazon.smithy.swift.codegen.SwiftDeclaration
1010
import software.amazon.smithy.swift.codegen.SwiftDependency
1111

1212
object SwiftTypes {
13+
val StringList =
14+
SwiftSymbol.make(
15+
"[String]",
16+
null,
17+
null,
18+
emptyList(),
19+
emptyList(),
20+
)
1321
val String = builtInSymbol("String", SwiftDeclaration.STRUCT)
1422
val Int = builtInSymbol("Int", SwiftDeclaration.STRUCT)
1523
val Int8 = builtInSymbol("Int8", SwiftDeclaration.STRUCT)

smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/HttpProtocolClientGeneratorTests.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ extension RestJsonProtocolClient {
4747
public var httpClientEngine: SmithyHTTPAPI.HTTPClient
4848
public var httpClientConfiguration: ClientRuntime.HttpClientConfiguration
4949
public var authSchemes: SmithyHTTPAuthAPI.AuthSchemes?
50+
public var authSchemePreference: [String]?
5051
public var authSchemeResolver: SmithyHTTPAuthAPI.AuthSchemeResolver
5152
public var bearerTokenIdentityResolver: any SmithyIdentity.BearerTokenIdentityResolver
5253
public private(set) var interceptorProviders: [ClientRuntime.InterceptorProvider]
@@ -61,6 +62,7 @@ extension RestJsonProtocolClient {
6162
_ httpClientEngine: SmithyHTTPAPI.HTTPClient,
6263
_ httpClientConfiguration: ClientRuntime.HttpClientConfiguration,
6364
_ authSchemes: SmithyHTTPAuthAPI.AuthSchemes?,
65+
_ authSchemePreference: [String]?,
6466
_ authSchemeResolver: SmithyHTTPAuthAPI.AuthSchemeResolver,
6567
_ bearerTokenIdentityResolver: any SmithyIdentity.BearerTokenIdentityResolver,
6668
_ interceptorProviders: [ClientRuntime.InterceptorProvider],
@@ -74,6 +76,7 @@ extension RestJsonProtocolClient {
7476
self.httpClientEngine = httpClientEngine
7577
self.httpClientConfiguration = httpClientConfiguration
7678
self.authSchemes = authSchemes
79+
self.authSchemePreference = authSchemePreference
7780
self.authSchemeResolver = authSchemeResolver
7881
self.bearerTokenIdentityResolver = bearerTokenIdentityResolver
7982
self.interceptorProviders = interceptorProviders
@@ -89,6 +92,7 @@ extension RestJsonProtocolClient {
8992
httpClientEngine: SmithyHTTPAPI.HTTPClient? = nil,
9093
httpClientConfiguration: ClientRuntime.HttpClientConfiguration? = nil,
9194
authSchemes: SmithyHTTPAuthAPI.AuthSchemes? = nil,
95+
authSchemePreference: [String]? = nil,
9296
authSchemeResolver: SmithyHTTPAuthAPI.AuthSchemeResolver? = nil,
9397
bearerTokenIdentityResolver: (any SmithyIdentity.BearerTokenIdentityResolver)? = nil,
9498
interceptorProviders: [ClientRuntime.InterceptorProvider]? = nil,
@@ -103,6 +107,7 @@ extension RestJsonProtocolClient {
103107
httpClientEngine ?? ClientRuntime.ClientConfigurationDefaults.makeClient(httpClientConfiguration: httpClientConfiguration ?? ClientRuntime.ClientConfigurationDefaults.defaultHttpClientConfiguration),
104108
httpClientConfiguration ?? ClientRuntime.ClientConfigurationDefaults.defaultHttpClientConfiguration,
105109
authSchemes ?? [],
110+
authSchemePreference ?? nil,
106111
authSchemeResolver ?? ClientRuntime.ClientConfigurationDefaults.defaultAuthSchemeResolver,
107112
bearerTokenIdentityResolver ?? SmithyIdentity.StaticBearerTokenIdentityResolver(token: SmithyIdentity.BearerTokenIdentity(token: "")),
108113
interceptorProviders ?? [],
@@ -120,6 +125,7 @@ extension RestJsonProtocolClient {
120125
httpClientEngine: nil,
121126
httpClientConfiguration: nil,
122127
authSchemes: nil,
128+
authSchemePreference: nil,
123129
authSchemeResolver: nil,
124130
bearerTokenIdentityResolver: nil,
125131
interceptorProviders: nil,
@@ -165,6 +171,7 @@ extension RestJsonProtocolClient {
165171
.withLogger(value: config.logger)
166172
.withPartitionID(value: config.partitionID)
167173
.withAuthSchemes(value: config.authSchemes ?? [])
174+
.withAuthSchemePreference(value: config.authSchemePreference)
168175
.withAuthSchemeResolver(value: config.authSchemeResolver)
169176
.withUnsignedPayloadTrait(value: false)
170177
.withSocketTimeout(value: config.httpClientConfiguration.socketTimeout)
@@ -197,6 +204,7 @@ extension RestJsonProtocolClient {
197204
.withLogger(value: config.logger)
198205
.withPartitionID(value: config.partitionID)
199206
.withAuthSchemes(value: config.authSchemes ?? [])
207+
.withAuthSchemePreference(value: config.authSchemePreference)
200208
.withAuthSchemeResolver(value: config.authSchemeResolver)
201209
.withUnsignedPayloadTrait(value: false)
202210
.withSocketTimeout(value: config.httpClientConfiguration.socketTimeout)

smithy-swift-codegen/src/test/kotlin/software/amazon/smithy/swift/codegen/requestandresponse/EventStreamTests.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ extension EventStreamTestClientTypes.TestStream {
220220
.withLogger(value: config.logger)
221221
.withPartitionID(value: config.partitionID)
222222
.withAuthSchemes(value: config.authSchemes ?? [])
223+
.withAuthSchemePreference(value: config.authSchemePreference)
223224
.withAuthSchemeResolver(value: config.authSchemeResolver)
224225
.withUnsignedPayloadTrait(value: false)
225226
.withSocketTimeout(value: config.httpClientConfiguration.socketTimeout)

0 commit comments

Comments
 (0)