Skip to content

feat: S3 Express support #1906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open

feat: S3 Express support #1906

wants to merge 45 commits into from

Conversation

jbelkins
Copy link
Contributor

@jbelkins jbelkins commented Mar 14, 2025

Description of changes

Provides customizations needed to utilize the S3 Express feature.

Companion S3 PR:
Includes integration tests:

  • Test that S3 Express client config features select the correct signing mode
  • Test of GetObject & PutObject presigned S3 Express URLs.
  • Test of several S3 operations on S3 Express-enabled buckets.

New/existing dependencies impact assessment, if applicable

No new dependencies were added to this change.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@@ -49,7 +50,7 @@ public class AWSSigV4Signer: SmithyHTTPAuthAPI.Signer {
)
}

guard let identity = identity as? AWSCredentialIdentity else {
guard let identity = (identity as? AWSCredentialIdentity) ?? (identity as? S3ExpressIdentity)?.awsCredentialIdentity else {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the CRT signer signs in S3 Express mode, it intakes the S3 Express credentials using the same Credentials type as with any other signing operation. So, when an S3ExpressIdentity is supplied, convert it to an AWS credential identity to pass into CRT (see private helper method below.)

import protocol SmithyHTTPAuthAPI.Signer
import struct Smithy.Attributes

public struct SigV4S3ExpressAuthScheme: AuthScheme {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This AuthScheme for S3 Express is basically the sigv4 auth scheme, but with a different scheme ID & signing algorithm.


import struct Smithy.AttributeKey

public enum AWSIdentityPropertyKeys {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These keys are used to pass data & components into identity resolvers in a type-safe manner.

clientConfig: DefaultClientConfiguration,
bucket: String
) async throws -> S3ExpressIdentity
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This protocol provides an interface for the S3 Express identity resolver to call S3 CreateSession to get credentials, without depending on the code-generated S3 client.

A SwiftIntegration on the S3 service provides conformance with this protocol.


import protocol SmithyIdentityAPI.Identity
import struct Foundation.Date

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A type to hold a S3 Express identity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though this type is more or less the same as AWSCredentialIdentity (minus only the account ID), a separate type is used for S3 Express credentials because the two credential types are not interchangeable, and separate types helps make clear which credentials are being provided.

import struct Smithy.Attributes
import protocol SmithyIdentityAPI.IdentityResolver

public protocol S3ExpressIdentityResolver: IdentityResolver where IdentityT == S3ExpressIdentity {}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A convenience declaration for the type of an S3 Express-specialized identity resolver.

authOption.signingProperties.set(key: SmithyHTTPAuthAPI.SigningPropertyKeys.signingRegion, value: param.signingRegion)
authOption.identityProperties.set(key: AWSSDKIdentity.AWSIdentityPropertyKeys.bucket, value: serviceParams.bucket)
authOption.identityProperties.set(key: AWSSDKIdentity.AWSIdentityPropertyKeys.s3ExpressClient, value: S3ExpressCreateSessionClient())
validAuthOptions.append(authOption)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Note: this is generated code]

An auth option is added for S3 Express. The bucket name and a S3ExpressCreateSessionClient are added to identity properties for use by the S3 Express identity resolver.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S3ExpressCreateSessionClient, which is added into identity properties above, is a wrapper around the S3 service client that allows runtime code to perform the S3 CreateSession operation for the purpose of obtaining S3 Express credentials, while preventing runtime from having to depend on the S3 client directly.

@@ -22522,6 +22523,7 @@ extension GetObjectInput {
.withExpiration(value: expiration)
.withIdentityResolver(value: config.awsCredentialIdentityResolver, schemeID: "aws.auth#sigv4")
.withIdentityResolver(value: config.awsCredentialIdentityResolver, schemeID: "aws.auth#sigv4a")
.withIdentityResolver(value: config.s3ExpressIdentityResolver, schemeID: "aws.auth#sigv4-s3express")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Note: this is generated code]

For this & several other presigned URLs:

  • The client config is added to context so it can be used for the S3 CreateSession operation to get credentials later on.
  • The S3 Express identity resolver is added to the operation.

@@ -44,7 +45,7 @@ public class DefaultAWSAuthSchemePlugin: ClientRuntime.Plugin {
public func configureClient(clientConfiguration: ClientRuntime.ClientConfiguration) throws {
if let config = clientConfiguration as? S3Client.S3ClientConfiguration {
config.authSchemeResolver = DefaultS3AuthSchemeResolver()
config.authSchemes = [AWSSDKHTTPAuth.SigV4AuthScheme(), AWSSDKHTTPAuth.SigV4AAuthScheme()]
config.authSchemes = [AWSSDKHTTPAuth.SigV4AuthScheme(), AWSSDKHTTPAuth.SigV4AAuthScheme(), AWSSDKHTTPAuth.SigV4S3ExpressAuthScheme()]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Note: this is generated code]

SigV4S3ExpressAuthScheme is added to the service's auth schemes


if (ctx.service.isS3) {
updatedAuthSchemeList += writer.format("\$N()", AWSSDKHTTPAuthTypes.SigV4S3ExpressAuthScheme)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SigV4S3ExpressAuthScheme is added, but only to the S3 service client.

@@ -36,6 +37,9 @@ abstract class AWSHTTPProtocolCustomizations : DefaultHTTPProtocolCustomizations
}
writer.write(" .withIdentityResolver(value: config.awsCredentialIdentityResolver, schemeID: \$S)", "aws.auth#sigv4")
writer.write(" .withIdentityResolver(value: config.awsCredentialIdentityResolver, schemeID: \$S)", "aws.auth#sigv4a")
if (ctx.service.isS3) {
writer.write(" .withIdentityResolver(value: config.s3ExpressIdentityResolver, schemeID: \$S)", "aws.auth#sigv4-s3express")
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S3 Express identity resolver is added to the context for an operation, but only to the S3 service client.

)
write("validAuthOptions.append(authOption)")
dedent()
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the S3 rules-based auth scheme resolver only, the S3 Express auth option is added.

(See the generated code above for more discussion of the content here.)

ctx: SwiftCodegenContext,
protocolGenerationContext: ProtocolGenerator.GenerationContext,
delegator: SwiftDelegator
) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method generates a file that creates a concrete implementation of the S3ExpressCreateSessionClient protocol.

An instance of this class is passed into the S3 credential provider, and allows that credential provider to call S3 without actually depending on S3.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.package(
id: "aws-sdk-swift.AWSSDKIdentity",
exact: "0.0.1"
),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here & below, the generated AWSS3 service client is shown. There are no changes in any other AWS service client.

import protocol ClientRuntime.DefaultClientConfiguration
import struct AWSSDKIdentity.S3ExpressIdentity


Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a code-generated, concrete implementation of the AWSSDKIdentity.S3ExpressCreateSessionClient protocol.

This serves as a "dependency inversion" wrapper to allow AWSSDKIdentity to call the S3 client without depending on it.

The method below takes a DefaultClientConfiguration instead of the concrete S3 configuration type, then it casts it to S3 config. Because this wrapper is only ever generated for S3, the cast to S3Client.Config should never fail.

@@ -125,6 +128,34 @@ class RulesBasedAuthSchemeResolverGenerator {
)
write("validAuthOptions.append(sigV4Option)")
dedent()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When S3 Express is used as the auth scheme, all the identity properties below need to be set so the S3 Express identity resolver has what it needs to get S3 Express credentials.

@jbelkins jbelkins marked this pull request as ready for review June 16, 2025 15:51
@jbelkins jbelkins requested review from dayaffe and sichanyoo June 16, 2025 15:51
Copy link
Contributor

@sichanyoo sichanyoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some Qs

Comment on lines +92 to +148
private func scheduleRefreshTask() {
// Cancel any previous refresh task
self.refreshTask?.cancel()

// Task captures self weakly, to prevent refreshing a cache that has been purged.
// self is then bound strongly both before & after the credential refresh wait.
self.refreshTask = Task { [weak self] in
guard let selfBeforeWait = self else { return }
let expiration: Date
do {
expiration = try await selfBeforeWait.retrieveTask.value.expiration ?? Date.distantPast
} catch {
// If there was an error retrieving credentials, throw it to the caller
// and remove this cached element.
// A subsequent access to credentials will create a new cached element.
await selfBeforeWait.removeSelfFromCache()
throw error
}

// Asynchronously wait until the credentials are within buffer time of expiring.
// Use the modern `ContinuousClock` time measurement if available.
let now = Date()
if #available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
let interval = expiration.timeIntervalSince(now) - Self.buffer
let duration = Duration(secondsComponent: Int64(interval), attosecondsComponent: 0)
let clock = ContinuousClock.now.advanced(by: duration)
let tolerance = Duration(secondsComponent: 1, attosecondsComponent: 0)
try await Task.sleep(until: clock, tolerance: tolerance, clock: .continuous)
} else {
let interval = expiration.timeIntervalSince(now) - Self.buffer
try await Task.sleep(nanoseconds: UInt64(interval) * 1_000_000_000)
}

// wait has now passed. Perform the credential refresh if credentials have been accessed,
// else this cached element will be purged for not being used.
guard !Task.isCancelled, let selfAfterWait = self else { return }
await selfAfterWait.performRefreshIfNeeded()
}
}

private func performRefreshIfNeeded() async {
switch status {
case .initial:
// This is the very first access to these credentials.
// If there isn't another access before refresh, these credentials will get purged.
status = .unaccessed
case .unaccessed:
// Credential hasn't been accessed since initial access.
// Purge this cached element from the cache.
await removeSelfFromCache()
case .accessed:
// Credential has been accessed since last refresh
// Refresh again.
self.status = .unaccessed
retrieveTask = Self.newRetrieveTask(identityProperties: identityProperties)
scheduleRefreshTask()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: What was the reason a simpler approach (where credentials themselves are directly cached & refreshed each time it's accessed if it's within refresh buffer window) was not taken?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered offline; because specifications require us to refresh credentials automatically in the background to avoid having to refresh credential at request time. Doing so reduces duration of API call.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed verbally, S3 Express credentials are refreshed on a timer, prior to expiration, so that fresh credentials are always available, preventing a S3 Express request from having to wait for credential refresh before it can proceed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants