From 253d329ec600a0650b00af8dfa06210e80bc570e Mon Sep 17 00:00:00 2001 From: George Fu Date: Fri, 2 Aug 2024 15:51:37 -0400 Subject: [PATCH] chore(id-auth-sra): S3 Express compatibility with ID & Auth SRA (#6346) * chore(id-auth-sra): remove s3 from useLegacyAuth list * chore(client-s3): turn off useLegacyAuth * chore(client-s3): id-auth sra and regionRedirect compatibility * chore(client-s3): compatibility for S3Express and httpsigning midware --- clients/client-s3/package.json | 2 +- clients/client-s3/src/S3Client.ts | 69 ++-- .../auth/httpAuthExtensionConfiguration.ts | 72 ++++ .../src/auth/httpAuthSchemeProvider.ts | 325 ++++++++++++++++++ .../client-s3/src/extensionConfiguration.ts | 5 +- clients/client-s3/src/models/models_0.ts | 1 - clients/client-s3/src/models/models_1.ts | 2 - clients/client-s3/src/runtimeConfig.shared.ts | 16 + clients/client-s3/src/runtimeExtensions.ts | 3 + clients/client-s3/test/unit/S3.spec.ts | 9 +- codegen/sdk-codegen/build.gradle.kts | 7 +- .../aws/typescript/codegen/AddS3Config.java | 7 + .../aws_sdk/AwsSdkSigV4Signer.ts | 19 +- .../httpAuthSchemes/aws_sdk/index.ts | 2 +- packages/middleware-sdk-s3/package.json | 3 + .../region-redirect-endpoint-middleware.ts | 34 +- .../s3ExpressHttpSigningMiddleware.ts | 124 +++++++ .../src/s3-express/functions/signS3Express.ts | 29 ++ .../middleware-sdk-s3/src/s3-express/index.ts | 5 + .../initializeWithMaximalConfiguration.ts | 4 +- 20 files changed, 690 insertions(+), 48 deletions(-) create mode 100644 clients/client-s3/src/auth/httpAuthExtensionConfiguration.ts create mode 100644 clients/client-s3/src/auth/httpAuthSchemeProvider.ts create mode 100644 packages/middleware-sdk-s3/src/s3-express/functions/s3ExpressHttpSigningMiddleware.ts create mode 100644 packages/middleware-sdk-s3/src/s3-express/functions/signS3Express.ts diff --git a/clients/client-s3/package.json b/clients/client-s3/package.json index cbd14725ead7..b1c8547bbcf9 100644 --- a/clients/client-s3/package.json +++ b/clients/client-s3/package.json @@ -38,7 +38,6 @@ "@aws-sdk/middleware-logger": "*", "@aws-sdk/middleware-recursion-detection": "*", "@aws-sdk/middleware-sdk-s3": "*", - "@aws-sdk/middleware-signing": "*", "@aws-sdk/middleware-ssec": "*", "@aws-sdk/middleware-user-agent": "*", "@aws-sdk/region-config-resolver": "*", @@ -76,6 +75,7 @@ "@smithy/util-defaults-mode-browser": "^3.0.14", "@smithy/util-defaults-mode-node": "^3.0.14", "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", "@smithy/util-retry": "^3.0.3", "@smithy/util-stream": "^3.1.3", "@smithy/util-utf8": "^3.0.0", diff --git a/clients/client-s3/src/S3Client.ts b/clients/client-s3/src/S3Client.ts index 998bbe60c324..40cfea69a470 100644 --- a/clients/client-s3/src/S3Client.ts +++ b/clients/client-s3/src/S3Client.ts @@ -10,26 +10,26 @@ import { getLoggerPlugin } from "@aws-sdk/middleware-logger"; import { getRecursionDetectionPlugin } from "@aws-sdk/middleware-recursion-detection"; import { getRegionRedirectMiddlewarePlugin, + getS3ExpressHttpSigningPlugin, getS3ExpressPlugin, getValidateBucketNamePlugin, resolveS3Config, S3InputConfig, S3ResolvedConfig, } from "@aws-sdk/middleware-sdk-s3"; -import { - AwsAuthInputConfig, - AwsAuthResolvedConfig, - getAwsAuthPlugin, - resolveAwsAuthConfig, -} from "@aws-sdk/middleware-signing"; import { getUserAgentPlugin, resolveUserAgentConfig, UserAgentInputConfig, UserAgentResolvedConfig, } from "@aws-sdk/middleware-user-agent"; -import { Credentials as __Credentials, GetAwsChunkedEncodingStream } from "@aws-sdk/types"; +import { GetAwsChunkedEncodingStream } from "@aws-sdk/types"; import { RegionInputConfig, RegionResolvedConfig, resolveRegionConfig } from "@smithy/config-resolver"; +import { + DefaultIdentityProviderConfig, + getHttpAuthSchemeEndpointRuleSetPlugin, + getHttpSigningPlugin, +} from "@smithy/core"; import { EventStreamSerdeInputConfig, EventStreamSerdeResolvedConfig, @@ -46,6 +46,7 @@ import { SmithyResolvedConfiguration as __SmithyResolvedConfiguration, } from "@smithy/smithy-client"; import { + AwsCredentialIdentityProvider, BodyLengthCalculator as __BodyLengthCalculator, CheckOptionalClientConfig as __CheckOptionalClientConfig, Checksum as __Checksum, @@ -68,6 +69,12 @@ import { } from "@smithy/types"; import { Readable } from "stream"; +import { + defaultS3HttpAuthSchemeParametersProvider, + HttpAuthSchemeInputConfig, + HttpAuthSchemeResolvedConfig, + resolveHttpAuthSchemeConfig, +} from "./auth/httpAuthSchemeProvider"; import { AbortMultipartUploadCommandInput, AbortMultipartUploadCommandOutput, @@ -663,19 +670,11 @@ export interface ClientDefaults extends Partial<__SmithyConfiguration<__HttpHand /** * Default credentials provider; Not available in browser runtime. + * @deprecated * @internal */ - credentialDefaultProvider?: (input: any) => __Provider<__Credentials>; + credentialDefaultProvider?: (input: any) => AwsCredentialIdentityProvider; - /** - * Whether to escape request path when signing the request. - */ - signingEscapePath?: boolean; - - /** - * Whether to override the request region with the region inferred from requested resource's ARN. Defaults to false. - */ - useArnRegion?: boolean | Provider; /** * Value for how many times a request will be made at most in case of retry. */ @@ -708,6 +707,15 @@ export interface ClientDefaults extends Partial<__SmithyConfiguration<__HttpHand */ defaultsMode?: __DefaultsMode | __Provider<__DefaultsMode>; + /** + * Whether to escape request path when signing the request. + */ + signingEscapePath?: boolean; + + /** + * Whether to override the request region with the region inferred from requested resource's ARN. Defaults to false. + */ + useArnRegion?: boolean | Provider; /** * The internal function that inject utilities to runtime-specific stream to help users consume the data * @internal @@ -725,9 +733,9 @@ export type S3ClientConfigType = Partial<__SmithyConfiguration<__HttpHandlerOpti RegionInputConfig & HostHeaderInputConfig & EndpointInputConfig & - AwsAuthInputConfig & - S3InputConfig & EventStreamSerdeInputConfig & + HttpAuthSchemeInputConfig & + S3InputConfig & ClientInputEndpointParameters; /** * @public @@ -747,9 +755,9 @@ export type S3ClientResolvedConfigType = __SmithyResolvedConfiguration<__HttpHan RegionResolvedConfig & HostHeaderResolvedConfig & EndpointResolvedConfig & - AwsAuthResolvedConfig & - S3ResolvedConfig & EventStreamSerdeResolvedConfig & + HttpAuthSchemeResolvedConfig & + S3ResolvedConfig & ClientResolvedEndpointParameters; /** * @public @@ -781,9 +789,9 @@ export class S3Client extends __Client< const _config_4 = resolveRegionConfig(_config_3); const _config_5 = resolveHostHeaderConfig(_config_4); const _config_6 = resolveEndpointConfig(_config_5); - const _config_7 = resolveAwsAuthConfig(_config_6); - const _config_8 = resolveS3Config(_config_7, { session: [() => this, CreateSessionCommand] }); - const _config_9 = resolveEventStreamSerdeConfig(_config_8); + const _config_7 = resolveEventStreamSerdeConfig(_config_6); + const _config_8 = resolveHttpAuthSchemeConfig(_config_7); + const _config_9 = resolveS3Config(_config_8, { session: [() => this, CreateSessionCommand] }); const _config_10 = resolveRuntimeExtensions(_config_9, configuration?.extensions || []); super(_config_10); this.config = _config_10; @@ -793,11 +801,22 @@ export class S3Client extends __Client< this.middlewareStack.use(getHostHeaderPlugin(this.config)); this.middlewareStack.use(getLoggerPlugin(this.config)); this.middlewareStack.use(getRecursionDetectionPlugin(this.config)); - this.middlewareStack.use(getAwsAuthPlugin(this.config)); + this.middlewareStack.use( + getHttpAuthSchemeEndpointRuleSetPlugin(this.config, { + httpAuthSchemeParametersProvider: defaultS3HttpAuthSchemeParametersProvider, + identityProviderConfigProvider: async (config: S3ClientResolvedConfig) => + new DefaultIdentityProviderConfig({ + "aws.auth#sigv4": config.credentials, + "aws.auth#sigv4a": config.credentials, + }), + }) + ); + this.middlewareStack.use(getHttpSigningPlugin(this.config)); this.middlewareStack.use(getValidateBucketNamePlugin(this.config)); this.middlewareStack.use(getAddExpectContinuePlugin(this.config)); this.middlewareStack.use(getRegionRedirectMiddlewarePlugin(this.config)); this.middlewareStack.use(getS3ExpressPlugin(this.config)); + this.middlewareStack.use(getS3ExpressHttpSigningPlugin(this.config)); } /** diff --git a/clients/client-s3/src/auth/httpAuthExtensionConfiguration.ts b/clients/client-s3/src/auth/httpAuthExtensionConfiguration.ts new file mode 100644 index 000000000000..2699fc3c838b --- /dev/null +++ b/clients/client-s3/src/auth/httpAuthExtensionConfiguration.ts @@ -0,0 +1,72 @@ +// smithy-typescript generated code +import { AwsCredentialIdentity, AwsCredentialIdentityProvider, HttpAuthScheme } from "@smithy/types"; + +import { S3HttpAuthSchemeProvider } from "./httpAuthSchemeProvider"; + +/** + * @internal + */ +export interface HttpAuthExtensionConfiguration { + setHttpAuthScheme(httpAuthScheme: HttpAuthScheme): void; + httpAuthSchemes(): HttpAuthScheme[]; + setHttpAuthSchemeProvider(httpAuthSchemeProvider: S3HttpAuthSchemeProvider): void; + httpAuthSchemeProvider(): S3HttpAuthSchemeProvider; + setCredentials(credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider): void; + credentials(): AwsCredentialIdentity | AwsCredentialIdentityProvider | undefined; +} + +/** + * @internal + */ +export type HttpAuthRuntimeConfig = Partial<{ + httpAuthSchemes: HttpAuthScheme[]; + httpAuthSchemeProvider: S3HttpAuthSchemeProvider; + credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider; +}>; + +/** + * @internal + */ +export const getHttpAuthExtensionConfiguration = ( + runtimeConfig: HttpAuthRuntimeConfig +): HttpAuthExtensionConfiguration => { + const _httpAuthSchemes = runtimeConfig.httpAuthSchemes!; + let _httpAuthSchemeProvider = runtimeConfig.httpAuthSchemeProvider!; + let _credentials = runtimeConfig.credentials; + return { + setHttpAuthScheme(httpAuthScheme: HttpAuthScheme): void { + const index = _httpAuthSchemes.findIndex((scheme) => scheme.schemeId === httpAuthScheme.schemeId); + if (index === -1) { + _httpAuthSchemes.push(httpAuthScheme); + } else { + _httpAuthSchemes.splice(index, 1, httpAuthScheme); + } + }, + httpAuthSchemes(): HttpAuthScheme[] { + return _httpAuthSchemes; + }, + setHttpAuthSchemeProvider(httpAuthSchemeProvider: S3HttpAuthSchemeProvider): void { + _httpAuthSchemeProvider = httpAuthSchemeProvider; + }, + httpAuthSchemeProvider(): S3HttpAuthSchemeProvider { + return _httpAuthSchemeProvider; + }, + setCredentials(credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider): void { + _credentials = credentials; + }, + credentials(): AwsCredentialIdentity | AwsCredentialIdentityProvider | undefined { + return _credentials; + }, + }; +}; + +/** + * @internal + */ +export const resolveHttpAuthRuntimeConfig = (config: HttpAuthExtensionConfiguration): HttpAuthRuntimeConfig => { + return { + httpAuthSchemes: config.httpAuthSchemes(), + httpAuthSchemeProvider: config.httpAuthSchemeProvider(), + credentials: config.credentials(), + }; +}; diff --git a/clients/client-s3/src/auth/httpAuthSchemeProvider.ts b/clients/client-s3/src/auth/httpAuthSchemeProvider.ts new file mode 100644 index 000000000000..8f4b81934a5c --- /dev/null +++ b/clients/client-s3/src/auth/httpAuthSchemeProvider.ts @@ -0,0 +1,325 @@ +// smithy-typescript generated code +import { + AwsSdkSigV4AuthInputConfig, + AwsSdkSigV4AuthResolvedConfig, + AwsSdkSigV4PreviouslyResolved, + resolveAwsSdkSigV4Config, +} from "@aws-sdk/core"; +import { signatureV4CrtContainer } from "@aws-sdk/signature-v4-multi-region"; +import { EndpointParameterInstructions, resolveParams } from "@smithy/middleware-endpoint"; +import { + EndpointV2, + HandlerExecutionContext, + HttpAuthOption, + HttpAuthScheme, + HttpAuthSchemeId, + HttpAuthSchemeParameters, + HttpAuthSchemeParametersProvider, + HttpAuthSchemeProvider, + Logger, +} from "@smithy/types"; +import { getSmithyContext, normalizeProvider } from "@smithy/util-middleware"; + +import { EndpointParameters } from "../endpoint/EndpointParameters"; +import { defaultEndpointResolver } from "../endpoint/endpointResolver"; +import { S3ClientConfig, S3ClientResolvedConfig } from "../S3Client"; + +/** + * @internal + */ +interface _S3HttpAuthSchemeParameters extends HttpAuthSchemeParameters { + region?: string; +} + +/** + * @internal + */ +export interface S3HttpAuthSchemeParameters extends _S3HttpAuthSchemeParameters, EndpointParameters { + region?: string; +} + +/** + * @internal + */ +export interface S3HttpAuthSchemeParametersProvider + extends HttpAuthSchemeParametersProvider< + S3ClientResolvedConfig, + HandlerExecutionContext, + S3HttpAuthSchemeParameters, + object + > {} + +/** + * @internal + */ +interface EndpointRuleSetSmithyContext { + commandInstance?: { + constructor?: { + getEndpointParameterInstructions(): EndpointParameterInstructions; + }; + }; +} +/** + * @internal + */ +interface EndpointRuleSetHttpAuthSchemeParametersProvider< + TConfig extends object, + TContext extends HandlerExecutionContext, + TParameters extends HttpAuthSchemeParameters & EndpointParameters, + TInput extends object +> extends HttpAuthSchemeParametersProvider {} +/** + * @internal + */ +const createEndpointRuleSetHttpAuthSchemeParametersProvider = + < + TConfig extends object, + TContext extends HandlerExecutionContext, + THttpAuthSchemeParameters extends HttpAuthSchemeParameters, + TEndpointParameters extends EndpointParameters, + TParameters extends THttpAuthSchemeParameters & TEndpointParameters, + TInput extends object + >( + defaultHttpAuthSchemeParametersProvider: HttpAuthSchemeParametersProvider< + TConfig, + TContext, + THttpAuthSchemeParameters, + TInput + > + ): EndpointRuleSetHttpAuthSchemeParametersProvider< + TConfig, + TContext, + THttpAuthSchemeParameters & TEndpointParameters, + TInput + > => + async (config: TConfig, context: TContext, input: TInput): Promise => { + if (!input) { + throw new Error(`Could not find \`input\` for \`defaultEndpointRuleSetHttpAuthSchemeParametersProvider\``); + } + const defaultParameters = await defaultHttpAuthSchemeParametersProvider(config, context, input); + const instructionsFn = (getSmithyContext(context) as EndpointRuleSetSmithyContext)?.commandInstance?.constructor + ?.getEndpointParameterInstructions; + if (!instructionsFn) { + throw new Error(`getEndpointParameterInstructions() is not defined on \`${context.commandName!}\``); + } + const endpointParameters = await resolveParams( + input as Record, + { getEndpointParameterInstructions: instructionsFn! }, + config as Record + ); + return Object.assign(defaultParameters, endpointParameters) as TParameters; + }; +/** + * @internal + */ +const _defaultS3HttpAuthSchemeParametersProvider = async ( + config: S3ClientResolvedConfig, + context: HandlerExecutionContext, + input: object +): Promise<_S3HttpAuthSchemeParameters> => { + return { + operation: getSmithyContext(context).operation as string, + region: + (await normalizeProvider(config.region)()) || + (() => { + throw new Error("expected `region` to be configured for `aws.auth#sigv4`"); + })(), + }; +}; +/** + * @internal + */ +export const defaultS3HttpAuthSchemeParametersProvider: S3HttpAuthSchemeParametersProvider = + createEndpointRuleSetHttpAuthSchemeParametersProvider(_defaultS3HttpAuthSchemeParametersProvider); + +function createAwsAuthSigv4HttpAuthOption(authParameters: S3HttpAuthSchemeParameters): HttpAuthOption { + return { + schemeId: "aws.auth#sigv4", + signingProperties: { + name: "s3", + region: authParameters.region, + }, + propertiesExtractor: (config: Partial, context) => ({ + /** + * @internal + */ + signingProperties: { + config, + context, + }, + }), + }; +} + +function createAwsAuthSigv4aHttpAuthOption(authParameters: S3HttpAuthSchemeParameters): HttpAuthOption { + return { + schemeId: "aws.auth#sigv4a", + signingProperties: { + name: "s3", + region: authParameters.region, + }, + propertiesExtractor: (config: Partial, context) => ({ + /** + * @internal + */ + signingProperties: { + config, + context, + }, + }), + }; +} + +/** + * @internal + */ +interface _S3HttpAuthSchemeProvider extends HttpAuthSchemeProvider {} + +/** + * @internal + */ +export interface S3HttpAuthSchemeProvider extends HttpAuthSchemeProvider {} + +/** + * @internal + */ +interface EndpointRuleSetHttpAuthSchemeProvider< + EndpointParametersT extends EndpointParameters, + HttpAuthSchemeParametersT extends HttpAuthSchemeParameters +> extends HttpAuthSchemeProvider {} +/** + * @internal + */ +interface DefaultEndpointResolver { + (params: EndpointParametersT, context?: { logger?: Logger }): EndpointV2; +} +/** + * @internal + */ +const createEndpointRuleSetHttpAuthSchemeProvider = < + EndpointParametersT extends EndpointParameters, + HttpAuthSchemeParametersT extends HttpAuthSchemeParameters +>( + defaultEndpointResolver: DefaultEndpointResolver, + defaultHttpAuthSchemeResolver: HttpAuthSchemeProvider, + createHttpAuthOptionFunctions: Record< + HttpAuthSchemeId, + (authParameters: EndpointParametersT & HttpAuthSchemeParametersT) => HttpAuthOption + > +): EndpointRuleSetHttpAuthSchemeProvider => { + const endpointRuleSetHttpAuthSchemeProvider: EndpointRuleSetHttpAuthSchemeProvider< + EndpointParametersT, + HttpAuthSchemeParametersT + > = (authParameters) => { + const endpoint: EndpointV2 = defaultEndpointResolver(authParameters); + const authSchemes = endpoint.properties?.authSchemes; + if (!authSchemes) { + return defaultHttpAuthSchemeResolver(authParameters); + } + const options: HttpAuthOption[] = []; + for (const scheme of authSchemes) { + const { name: resolvedName, properties = {}, ...rest } = scheme; + const name = resolvedName.toLowerCase(); + if (resolvedName !== name) { + console.warn(`HttpAuthScheme has been normalized with lowercasing: \`${resolvedName}\` to \`${name}\``); + } + let schemeId; + if (name === "sigv4a") { + schemeId = "aws.auth#sigv4a"; + const sigv4Present = authSchemes.find((s) => { + const name = s.name.toLowerCase(); + return name !== "sigv4a" && name.startsWith("sigv4"); + }); + if (!signatureV4CrtContainer.CrtSignerV4 && sigv4Present) { + // sigv4a -> sigv4 fallback. + continue; + } + } else if (name.startsWith("sigv4")) { + schemeId = "aws.auth#sigv4"; + } else { + throw new Error(`Unknown HttpAuthScheme found in \`@smithy.rules#endpointRuleSet\`: \`${name}\``); + } + const createOption = createHttpAuthOptionFunctions[schemeId]; + if (!createOption) { + throw new Error(`Could not find HttpAuthOption create function for \`${schemeId}\``); + } + const option = createOption(authParameters); + option.schemeId = schemeId; + option.signingProperties = { ...(option.signingProperties || {}), ...rest, ...properties }; + options.push(option); + } + return options; + }; + + return endpointRuleSetHttpAuthSchemeProvider; +}; +/** + * @internal + */ +const _defaultS3HttpAuthSchemeProvider: _S3HttpAuthSchemeProvider = (authParameters) => { + const options: HttpAuthOption[] = []; + switch (authParameters.operation) { + default: { + options.push(createAwsAuthSigv4HttpAuthOption(authParameters)); + options.push(createAwsAuthSigv4aHttpAuthOption(authParameters)); + } + } + return options; +}; +/** + * @internal + */ +export const defaultS3HttpAuthSchemeProvider: S3HttpAuthSchemeProvider = createEndpointRuleSetHttpAuthSchemeProvider( + defaultEndpointResolver, + _defaultS3HttpAuthSchemeProvider, + { + "aws.auth#sigv4": createAwsAuthSigv4HttpAuthOption, + "aws.auth#sigv4a": createAwsAuthSigv4aHttpAuthOption, + } +); + +/** + * @internal + */ +export interface HttpAuthSchemeInputConfig extends AwsSdkSigV4AuthInputConfig { + /** + * Configuration of HttpAuthSchemes for a client which provides default identity providers and signers per auth scheme. + * @internal + */ + httpAuthSchemes?: HttpAuthScheme[]; + + /** + * Configuration of an HttpAuthSchemeProvider for a client which resolves which HttpAuthScheme to use. + * @internal + */ + httpAuthSchemeProvider?: S3HttpAuthSchemeProvider; +} + +/** + * @internal + */ +export interface HttpAuthSchemeResolvedConfig extends AwsSdkSigV4AuthResolvedConfig { + /** + * Configuration of HttpAuthSchemes for a client which provides default identity providers and signers per auth scheme. + * @internal + */ + readonly httpAuthSchemes: HttpAuthScheme[]; + + /** + * Configuration of an HttpAuthSchemeProvider for a client which resolves which HttpAuthScheme to use. + * @internal + */ + readonly httpAuthSchemeProvider: S3HttpAuthSchemeProvider; +} + +/** + * @internal + */ +export const resolveHttpAuthSchemeConfig = ( + config: T & HttpAuthSchemeInputConfig & AwsSdkSigV4PreviouslyResolved +): T & HttpAuthSchemeResolvedConfig => { + const config_0 = resolveAwsSdkSigV4Config(config); + return { + ...config_0, + } as T & HttpAuthSchemeResolvedConfig; +}; diff --git a/clients/client-s3/src/extensionConfiguration.ts b/clients/client-s3/src/extensionConfiguration.ts index 7f8cdb6615a8..5acc0f2effcc 100644 --- a/clients/client-s3/src/extensionConfiguration.ts +++ b/clients/client-s3/src/extensionConfiguration.ts @@ -3,10 +3,13 @@ import { AwsRegionExtensionConfiguration } from "@aws-sdk/types"; import { HttpHandlerExtensionConfiguration } from "@smithy/protocol-http"; import { DefaultExtensionConfiguration } from "@smithy/types"; +import { HttpAuthExtensionConfiguration } from "./auth/httpAuthExtensionConfiguration"; + /** * @internal */ export interface S3ExtensionConfiguration extends HttpHandlerExtensionConfiguration, DefaultExtensionConfiguration, - AwsRegionExtensionConfiguration {} + AwsRegionExtensionConfiguration, + HttpAuthExtensionConfiguration {} diff --git a/clients/client-s3/src/models/models_0.ts b/clients/client-s3/src/models/models_0.ts index 6f0d5a2a4d22..1fc23941642d 100644 --- a/clients/client-s3/src/models/models_0.ts +++ b/clients/client-s3/src/models/models_0.ts @@ -1,6 +1,5 @@ // smithy-typescript generated code import { ExceptionOptionType as __ExceptionOptionType, SENSITIVE_STRING } from "@smithy/smithy-client"; - import { StreamingBlobTypes } from "@smithy/types"; import { S3ServiceException as __BaseException } from "./S3ServiceException"; diff --git a/clients/client-s3/src/models/models_1.ts b/clients/client-s3/src/models/models_1.ts index b66d7727cacd..96378677b77e 100644 --- a/clients/client-s3/src/models/models_1.ts +++ b/clients/client-s3/src/models/models_1.ts @@ -1,6 +1,5 @@ // smithy-typescript generated code import { ExceptionOptionType as __ExceptionOptionType, SENSITIVE_STRING } from "@smithy/smithy-client"; - import { StreamingBlobTypes } from "@smithy/types"; import { @@ -28,7 +27,6 @@ import { StorageClass, Tag, } from "./models_0"; - import { S3ServiceException as __BaseException } from "./S3ServiceException"; /** diff --git a/clients/client-s3/src/runtimeConfig.shared.ts b/clients/client-s3/src/runtimeConfig.shared.ts index 7be05924efe9..62c54fffa026 100644 --- a/clients/client-s3/src/runtimeConfig.shared.ts +++ b/clients/client-s3/src/runtimeConfig.shared.ts @@ -1,11 +1,14 @@ // smithy-typescript generated code +import { AwsSdkSigV4ASigner, AwsSdkSigV4Signer } from "@aws-sdk/core"; import { SignatureV4MultiRegion } from "@aws-sdk/signature-v4-multi-region"; import { NoOpLogger } from "@smithy/smithy-client"; +import { IdentityProviderConfig } from "@smithy/types"; import { parseUrl } from "@smithy/url-parser"; import { fromBase64, toBase64 } from "@smithy/util-base64"; import { getAwsChunkedEncodingStream, sdkStreamMixin } from "@smithy/util-stream"; import { fromUtf8, toUtf8 } from "@smithy/util-utf8"; +import { defaultS3HttpAuthSchemeProvider } from "./auth/httpAuthSchemeProvider"; import { defaultEndpointResolver } from "./endpoint/endpointResolver"; import { S3ClientConfig } from "./S3Client"; @@ -21,6 +24,19 @@ export const getRuntimeConfig = (config: S3ClientConfig) => { endpointProvider: config?.endpointProvider ?? defaultEndpointResolver, extensions: config?.extensions ?? [], getAwsChunkedEncodingStream: config?.getAwsChunkedEncodingStream ?? getAwsChunkedEncodingStream, + httpAuthSchemeProvider: config?.httpAuthSchemeProvider ?? defaultS3HttpAuthSchemeProvider, + httpAuthSchemes: config?.httpAuthSchemes ?? [ + { + schemeId: "aws.auth#sigv4", + identityProvider: (ipc: IdentityProviderConfig) => ipc.getIdentityProvider("aws.auth#sigv4"), + signer: new AwsSdkSigV4Signer(), + }, + { + schemeId: "aws.auth#sigv4a", + identityProvider: (ipc: IdentityProviderConfig) => ipc.getIdentityProvider("aws.auth#sigv4a"), + signer: new AwsSdkSigV4ASigner(), + }, + ], logger: config?.logger ?? new NoOpLogger(), sdkStreamMixin: config?.sdkStreamMixin ?? sdkStreamMixin, serviceId: config?.serviceId ?? "S3", diff --git a/clients/client-s3/src/runtimeExtensions.ts b/clients/client-s3/src/runtimeExtensions.ts index 454a50e80734..9d8607cc18ec 100644 --- a/clients/client-s3/src/runtimeExtensions.ts +++ b/clients/client-s3/src/runtimeExtensions.ts @@ -6,6 +6,7 @@ import { import { getHttpHandlerExtensionConfiguration, resolveHttpHandlerRuntimeConfig } from "@smithy/protocol-http"; import { getDefaultExtensionConfiguration, resolveDefaultRuntimeConfig } from "@smithy/smithy-client"; +import { getHttpAuthExtensionConfiguration, resolveHttpAuthRuntimeConfig } from "./auth/httpAuthExtensionConfiguration"; import { S3ExtensionConfiguration } from "./extensionConfiguration"; /** @@ -32,6 +33,7 @@ export const resolveRuntimeExtensions = (runtimeConfig: any, extensions: Runtime ...asPartial(getAwsRegionExtensionConfiguration(runtimeConfig)), ...asPartial(getDefaultExtensionConfiguration(runtimeConfig)), ...asPartial(getHttpHandlerExtensionConfiguration(runtimeConfig)), + ...asPartial(getHttpAuthExtensionConfiguration(runtimeConfig)), }; extensions.forEach((extension) => extension.configure(extensionConfiguration)); @@ -41,5 +43,6 @@ export const resolveRuntimeExtensions = (runtimeConfig: any, extensions: Runtime ...resolveAwsRegionExtensionConfiguration(extensionConfiguration), ...resolveDefaultRuntimeConfig(extensionConfiguration), ...resolveHttpHandlerRuntimeConfig(extensionConfiguration), + ...resolveHttpAuthRuntimeConfig(extensionConfiguration), }; }; diff --git a/clients/client-s3/test/unit/S3.spec.ts b/clients/client-s3/test/unit/S3.spec.ts index 34bc56d2ecc5..70bb865f208f 100644 --- a/clients/client-s3/test/unit/S3.spec.ts +++ b/clients/client-s3/test/unit/S3.spec.ts @@ -86,8 +86,9 @@ describe("Endpoints from ARN", () => { const OutpostId = "op-01234567890123456"; const AccountId = "123456789012"; const region = "us-west-2"; + const clientRegion = "us-east-1"; const credentials = { accessKeyId: "key", secretAccessKey: "secret" }; - const client = new S3({ region: "us-east-1", credentials, useArnRegion: true }); + const client = new S3({ region: clientRegion, credentials, useArnRegion: true }); client.middlewareStack.add(endpointValidator, { step: "finalizeRequest", priority: "low" }); const result: any = await client.putObject({ Bucket: `arn:aws:s3-outposts:${region}:${AccountId}:outpost/${OutpostId}/accesspoint/abc-111`, @@ -96,6 +97,12 @@ describe("Endpoints from ARN", () => { }); expect(result.request.hostname).to.eql(`abc-111-${AccountId}.${OutpostId}.s3-outposts.us-west-2.amazonaws.com`); const date = new Date().toISOString().slice(0, 10).replace(/-/g, ""); //20201029 + + /* + * Due to sigv4a -> sigv4 fallback, without a sigv4a implementation installed (it's optional) + * the credential should contain the ARN region, which is us-west-2, and not + * the us-east-1 region used by the client. + */ expect(result.request.headers["authorization"]).contains( `Credential=${credentials.accessKeyId}/${date}/${region}/s3-outposts/aws4_request` ); diff --git a/codegen/sdk-codegen/build.gradle.kts b/codegen/sdk-codegen/build.gradle.kts index d35907d2b080..f44c63022f03 100644 --- a/codegen/sdk-codegen/build.gradle.kts +++ b/codegen/sdk-codegen/build.gradle.kts @@ -106,9 +106,8 @@ tasks.register("generate-smithy-build") { File("smithy-aws-typescript-codegen/src/main/resources/software/amazon/smithy/aws/typescript/codegen/package.json.template") .readText() ).expectObjectNode() - val nonExperimentalIdentityAndAuthServices = setOf( - // S3 - "S3", + val useLegacyAuthServices = setOf( + // e.g. "S3" - use this as exclusion list if needed. ) val projectionContents = Node.objectNodeBuilder() .withMember("imports", Node.fromStrings("${models.getAbsolutePath()}${File.separator}${file.name}")) @@ -121,7 +120,7 @@ tasks.register("generate-smithy-build") { .withMember("packageDescription", "AWS SDK for JavaScript " + clientName + " Client for Node.js, Browser and React Native") .withMember("useLegacyAuth", - nonExperimentalIdentityAndAuthServices.contains(serviceTrait.sdkId)) + useLegacyAuthServices.contains(serviceTrait.sdkId)) .build())) .build() projectionsBuilder.withMember(sdkId + "." + version.toLowerCase(), projectionContents) diff --git a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddS3Config.java b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddS3Config.java index 45a9c95a05b5..bf899d38dc3b 100644 --- a/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddS3Config.java +++ b/codegen/smithy-aws-typescript-codegen/src/main/java/software/amazon/smithy/aws/typescript/codegen/AddS3Config.java @@ -50,6 +50,7 @@ import software.amazon.smithy.typescript.codegen.TypeScriptDependency; import software.amazon.smithy.typescript.codegen.TypeScriptSettings; import software.amazon.smithy.typescript.codegen.TypeScriptWriter; +import software.amazon.smithy.typescript.codegen.auth.http.integration.AddHttpSigningPlugin; import software.amazon.smithy.typescript.codegen.integration.RuntimeClientPlugin; import software.amazon.smithy.typescript.codegen.integration.TypeScriptIntegration; import software.amazon.smithy.utils.ListUtils; @@ -81,6 +82,7 @@ public final class AddS3Config implements TypeScriptIntegration { @Override public List runAfter() { return List.of( + new AddHttpSigningPlugin().name(), AddBuiltinPlugins.class.getCanonicalName(), AddEndpointsPlugin.class.getCanonicalName() ); @@ -398,6 +400,11 @@ && containsInputMembers(m, o, BUCKET_ENDPOINT_INPUT_KEYS)) .withConventions(AwsDependency.S3_MIDDLEWARE.dependency, "S3Express", HAS_MIDDLEWARE) .servicePredicate((m, s) -> isS3(s) && isEndpointsV2Service(s)) + .build(), + RuntimeClientPlugin.builder() + .withConventions(AwsDependency.S3_MIDDLEWARE.dependency, "S3ExpressHttpSigning", + HAS_MIDDLEWARE) + .servicePredicate((m, s) -> isS3(s) && isEndpointsV2Service(s)) .build() ); } diff --git a/packages/core/src/submodules/httpAuthSchemes/aws_sdk/AwsSdkSigV4Signer.ts b/packages/core/src/submodules/httpAuthSchemes/aws_sdk/AwsSdkSigV4Signer.ts index de78134d8824..b0f7d5245d93 100644 --- a/packages/core/src/submodules/httpAuthSchemes/aws_sdk/AwsSdkSigV4Signer.ts +++ b/packages/core/src/submodules/httpAuthSchemes/aws_sdk/AwsSdkSigV4Signer.ts @@ -97,7 +97,24 @@ export class AwsSdkSigV4Signer implements HttpSigner { if (!HttpRequest.isInstance(httpRequest)) { throw new Error("The request is not an instance of `HttpRequest` and cannot be signed"); } - const { config, signer, signingRegion, signingName } = await validateSigningProperties(signingProperties); + + const validatedProps = await validateSigningProperties(signingProperties); + + const { config, signer } = validatedProps; + let { signingRegion, signingName } = validatedProps; + + const handlerExecutionContext = signingProperties.context as HandlerExecutionContext; + + if (handlerExecutionContext?.authSchemes?.length ?? 0 > 1) { + const [first, second] = handlerExecutionContext.authSchemes!; + // since this is not the sigv4a signer, we accept the secondary authscheme's signing data + // if the first authscheme is sigv4a and second is sigv4. + if (first?.name === "sigv4a" && second?.name === "sigv4") { + signingRegion = second?.signingRegion ?? signingRegion; + signingName = second?.signingName ?? signingName; + } + } + const signedRequest = await signer.sign(httpRequest, { signingDate: getSkewCorrectedDate(config.systemClockOffset), signingRegion: signingRegion, diff --git a/packages/core/src/submodules/httpAuthSchemes/aws_sdk/index.ts b/packages/core/src/submodules/httpAuthSchemes/aws_sdk/index.ts index 98b5ca4fecad..62172b787785 100644 --- a/packages/core/src/submodules/httpAuthSchemes/aws_sdk/index.ts +++ b/packages/core/src/submodules/httpAuthSchemes/aws_sdk/index.ts @@ -1,3 +1,3 @@ -export { AwsSdkSigV4Signer, AWSSDKSigV4Signer } from "./AwsSdkSigV4Signer"; +export { AwsSdkSigV4Signer, AWSSDKSigV4Signer, validateSigningProperties } from "./AwsSdkSigV4Signer"; export { AwsSdkSigV4ASigner } from "./AwsSdkSigV4ASigner"; export * from "./resolveAwsSdkSigV4Config"; diff --git a/packages/middleware-sdk-s3/package.json b/packages/middleware-sdk-s3/package.json index b63b84bd6d0b..55ee0dccffc2 100644 --- a/packages/middleware-sdk-s3/package.json +++ b/packages/middleware-sdk-s3/package.json @@ -23,14 +23,17 @@ }, "license": "Apache-2.0", "dependencies": { + "@aws-sdk/core": "*", "@aws-sdk/types": "*", "@aws-sdk/util-arn-parser": "*", + "@smithy/core": "^2.3.2", "@smithy/node-config-provider": "^3.1.4", "@smithy/protocol-http": "^4.1.0", "@smithy/signature-v4": "^4.1.0", "@smithy/smithy-client": "^3.1.12", "@smithy/types": "^3.3.0", "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", "@smithy/util-stream": "^3.1.3", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" diff --git a/packages/middleware-sdk-s3/src/region-redirect-endpoint-middleware.ts b/packages/middleware-sdk-s3/src/region-redirect-endpoint-middleware.ts index ece8a964ace1..9382537a7d54 100644 --- a/packages/middleware-sdk-s3/src/region-redirect-endpoint-middleware.ts +++ b/packages/middleware-sdk-s3/src/region-redirect-endpoint-middleware.ts @@ -21,20 +21,34 @@ export const regionRedirectEndpointMiddleware = (config: PreviouslyResolved): Se async (args: SerializeHandlerArguments): Promise> => { const originalRegion = await config.region(); const regionProviderRef = config.region; + let unlock = () => {}; if (context.__s3RegionRedirect) { - config.region = async () => { - config.region = regionProviderRef; - return context.__s3RegionRedirect; - }; + Object.defineProperty(config, "region", { + writable: false, + value: async () => { + return context.__s3RegionRedirect; + }, + }); + unlock = () => + Object.defineProperty(config, "region", { + writable: true, + value: regionProviderRef, + }); } - const result = await next(args); - if (context.__s3RegionRedirect) { - const region = await config.region(); - if (originalRegion !== region) { - throw new Error("Region was not restored following S3 region redirect."); + try { + const result = await next(args); + if (context.__s3RegionRedirect) { + unlock(); + const region = await config.region(); + if (originalRegion !== region) { + throw new Error("Region was not restored following S3 region redirect."); + } } + return result; + } catch (e: unknown) { + unlock(); + throw e; } - return result; }; }; diff --git a/packages/middleware-sdk-s3/src/s3-express/functions/s3ExpressHttpSigningMiddleware.ts b/packages/middleware-sdk-s3/src/s3-express/functions/s3ExpressHttpSigningMiddleware.ts new file mode 100644 index 000000000000..8d5509108440 --- /dev/null +++ b/packages/middleware-sdk-s3/src/s3-express/functions/s3ExpressHttpSigningMiddleware.ts @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { httpSigningMiddlewareOptions } from "@smithy/core"; +import { HttpRequest, IHttpRequest } from "@smithy/protocol-http"; +import { + AuthScheme, + AwsCredentialIdentity, + ErrorHandler, + FinalizeHandler, + FinalizeHandlerArguments, + FinalizeHandlerOutput, + FinalizeRequestMiddleware, + HandlerExecutionContext, + Pluggable, + RequestSigner, + SelectedHttpAuthScheme, + SMITHY_CONTEXT_KEY, + SuccessHandler, +} from "@smithy/types"; +import { getSmithyContext } from "@smithy/util-middleware"; + +import { signS3Express } from "./signS3Express"; + +/** + * @internal + */ +interface HttpSigningMiddlewareSmithyContext extends Record { + selectedHttpAuthScheme?: SelectedHttpAuthScheme; +} + +/** + * @internal + */ +interface HttpSigningMiddlewareHandlerExecutionContext extends HandlerExecutionContext { + [SMITHY_CONTEXT_KEY]?: HttpSigningMiddlewareSmithyContext; +} + +const defaultErrorHandler: ErrorHandler = (signingProperties) => (error) => { + throw error; +}; + +const defaultSuccessHandler: SuccessHandler = ( + httpResponse: unknown, + signingProperties: Record +): void => {}; + +interface SigningProperties { + signingRegion: string; + signingDate: Date; + signingService: string; +} + +interface PreviouslyResolved { + signer: (authScheme?: AuthScheme | undefined) => Promise< + RequestSigner & { + signWithCredentials( + req: IHttpRequest, + identity: AwsCredentialIdentity, + opts?: Partial + ): Promise; + } + >; +} + +/** + * @internal + */ +export const s3ExpressHttpSigningMiddlewareOptions = httpSigningMiddlewareOptions; + +/** + * @internal + */ +export const s3ExpressHttpSigningMiddleware = + (config: PreviouslyResolved): FinalizeRequestMiddleware => + (next: FinalizeHandler, context: HttpSigningMiddlewareHandlerExecutionContext): FinalizeHandler => + async (args: FinalizeHandlerArguments): Promise> => { + if (!HttpRequest.isInstance(args.request)) { + return next(args); + } + + const smithyContext: HttpSigningMiddlewareSmithyContext = getSmithyContext(context); + const scheme = smithyContext.selectedHttpAuthScheme; + if (!scheme) { + throw new Error(`No HttpAuthScheme was selected: unable to sign request`); + } + const { + httpAuthOption: { signingProperties = {} }, + identity, + signer, + } = scheme; + + let request: IHttpRequest; + + if (context.s3ExpressIdentity) { + request = await signS3Express( + context.s3ExpressIdentity, + signingProperties as unknown as SigningProperties, + args.request, + await config.signer() + ); + } else { + request = await signer.sign(args.request, identity, signingProperties); + } + + const output = await next({ + ...args, + request, + }).catch((signer.errorHandler || defaultErrorHandler)(signingProperties)); + (signer.successHandler || defaultSuccessHandler)(output.response, signingProperties); + return output; + }; + +/** + * @internal + */ +export const getS3ExpressHttpSigningPlugin = (config: { + signer: (authScheme?: AuthScheme | undefined) => Promise; +}): Pluggable => ({ + applyToStack: (clientStack) => { + clientStack.addRelativeTo( + s3ExpressHttpSigningMiddleware(config as PreviouslyResolved), + httpSigningMiddlewareOptions + ); + }, +}); diff --git a/packages/middleware-sdk-s3/src/s3-express/functions/signS3Express.ts b/packages/middleware-sdk-s3/src/s3-express/functions/signS3Express.ts new file mode 100644 index 000000000000..9be783b8c161 --- /dev/null +++ b/packages/middleware-sdk-s3/src/s3-express/functions/signS3Express.ts @@ -0,0 +1,29 @@ +import type { AwsCredentialIdentity, HttpRequest as IHttpRequest } from "@smithy/types"; + +import { S3ExpressIdentity } from "../interfaces/S3ExpressIdentity"; + +export const signS3Express = async ( + s3ExpressIdentity: S3ExpressIdentity, + signingOptions: { + signingDate: Date; + signingRegion: string; + signingService: string; + }, + request: IHttpRequest, + sigV4MultiRegionSigner: { + signWithCredentials( + req: IHttpRequest, + identity: AwsCredentialIdentity, + opts?: Partial + ): Promise; + } +) => { + // the signer is expected to be SignatureV4MultiRegion for S3. + const signedRequest = await sigV4MultiRegionSigner.signWithCredentials(request, s3ExpressIdentity, {}); + + if (signedRequest.headers["X-Amz-Security-Token"] || signedRequest.headers["x-amz-security-token"]) { + throw new Error("X-Amz-Security-Token must not be set for s3-express requests."); + } + + return signedRequest; +}; diff --git a/packages/middleware-sdk-s3/src/s3-express/index.ts b/packages/middleware-sdk-s3/src/s3-express/index.ts index ef05b7d1748c..6d3b3fceb4d6 100644 --- a/packages/middleware-sdk-s3/src/s3-express/index.ts +++ b/packages/middleware-sdk-s3/src/s3-express/index.ts @@ -4,5 +4,10 @@ export { S3ExpressIdentityProviderImpl } from "./classes/S3ExpressIdentityProvid export { SignatureV4S3Express } from "./classes/SignatureV4S3Express"; export { NODE_DISABLE_S3_EXPRESS_SESSION_AUTH_OPTIONS } from "./constants"; export { getS3ExpressPlugin, s3ExpressMiddleware, s3ExpressMiddlewareOptions } from "./functions/s3ExpressMiddleware"; +export { + getS3ExpressHttpSigningPlugin, + s3ExpressHttpSigningMiddleware, + s3ExpressHttpSigningMiddlewareOptions, +} from "./functions/s3ExpressHttpSigningMiddleware"; export { S3ExpressIdentity } from "./interfaces/S3ExpressIdentity"; export { S3ExpressIdentityProvider } from "./interfaces/S3ExpressIdentityProvider"; diff --git a/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts b/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts index 0944609ae18d..4a8a568ef6d8 100644 --- a/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts +++ b/private/aws-client-api-test/src/client-interface-tests/client-s3/impl/initializeWithMaximalConfiguration.ts @@ -17,7 +17,7 @@ import { NODE_MAX_ATTEMPT_CONFIG_OPTIONS, NODE_RETRY_MODE_CONFIG_OPTIONS } from import { loadConfig as loadNodeConfig } from "@smithy/node-config-provider"; import { NodeHttpHandler, streamCollector } from "@smithy/node-http-handler"; import { loadConfigsForDefaultMode } from "@smithy/smithy-client"; -import { EndpointV2 } from "@smithy/types"; +import { EndpointV2, HttpAuthSchemeProvider } from "@smithy/types"; import { parseUrl } from "@smithy/url-parser"; import { fromBase64, toBase64 } from "@smithy/util-base64"; import { calculateBodyLength } from "@smithy/util-body-length-node"; @@ -104,6 +104,8 @@ export const initializeWithMaximalConfiguration = () => { streamHasher: streamHasher, utf8Decoder: fromUtf8, utf8Encoder: toUtf8, + httpAuthSchemes: [], + httpAuthSchemeProvider: (() => null) as unknown as HttpAuthSchemeProvider, // END internal options // S3 specific options below