diff --git a/README.md b/README.md index cfb3e60d14b..66f872bb639 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,38 @@ Simply downcase the service module name for the helper: * `ec2` => `#` * etc +## Functionality requiring AWS Common Runtime (CRT) + +The AWS SDK for Ruby has optional functionality that requires the +[AWS Common Runtime (CRT)](https://docs.aws.amazon.com/sdkref/latest/guide/common-runtime.html) +bindings to be included as a dependency with your application. This functionality includes: +* [Amazon S3 Multi-Region Access Points](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiRegionAccessPoints.html) +* CRC-32c support for [S3 Additional Checksums](https://aws.amazon.com/blogs/aws/new-additional-checksum-algorithms-for-amazon-s3/) + +If the required AWS Common Runtime dependency is not installed you will receive an error +indicating that the required dependency is missing to use the associated functionality. To install this dependency follow +the provided [instructions](#installing-the-aws-common-runtime-crt-dependency). + +### Installing the AWS Common Runtime (CRT) Dependency +AWS CRT bindings are in developer preview and are available in the +the [aws-crt](https://rubygems.org/gems/aws-crt/) gem. You can install them +by adding the `aws-crt` gem to your Gemfile. + +[Sigv4a](https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html) +is an extension to Sigv4 that allows signatures that are valid in more than one region. +Sigv4a is required to use some services/operations such as +[S3 Multi-Region Access Points](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiRegionAccessPoints.html) +and Amazon EventBridge Global Endpoints. +Currently sigv4a requires the [aws-crt](https://rubygems.org/gems/aws-crt/) gem and a version of the +[aws-sigv4](https://rubygems.org/gems/aws-sigv4/versions/1.4.1.crt) gem built on top of aws-crt - +these versions end with "-crt". To install and use a CRT enabled version, we recommend pinning the +specific version of `aws-sigv4` in your Gemfile (this will also install the `aws-crt` gem): + +```ruby +gem 'aws-sdk-s3', '~> 1' +gem 'aws-sigv4', '1.4.1.crt' +``` + ## Getting Help Please use any of these resources for getting help: diff --git a/gems/aws-sdk-eventbridge/lib/aws-sdk-eventbridge/client.rb b/gems/aws-sdk-eventbridge/lib/aws-sdk-eventbridge/client.rb index 451e374b8fa..81fa626f28f 100644 --- a/gems/aws-sdk-eventbridge/lib/aws-sdk-eventbridge/client.rb +++ b/gems/aws-sdk-eventbridge/lib/aws-sdk-eventbridge/client.rb @@ -32,6 +32,7 @@ require 'aws-sdk-core/plugins/recursion_detection.rb' require 'aws-sdk-core/plugins/signature_v4.rb' require 'aws-sdk-core/plugins/protocols/json_rpc.rb' +require 'aws-sdk-eventbridge/plugins/multi_region_endpoint.rb' Aws::Plugins::GlobalConfiguration.add_identifier(:eventbridge) @@ -81,6 +82,7 @@ class Client < Seahorse::Client::Base add_plugin(Aws::Plugins::RecursionDetection) add_plugin(Aws::Plugins::SignatureV4) add_plugin(Aws::Plugins::Protocols::JsonRpc) + add_plugin(Aws::EventBridge::Plugins::MultiRegionEndpoint) # @overload initialize(options) # @param [Hash] options diff --git a/gems/aws-sdk-eventbridge/lib/aws-sdk-eventbridge/plugins/multi_region_endpoint.rb b/gems/aws-sdk-eventbridge/lib/aws-sdk-eventbridge/plugins/multi_region_endpoint.rb new file mode 100644 index 00000000000..dc593efaec2 --- /dev/null +++ b/gems/aws-sdk-eventbridge/lib/aws-sdk-eventbridge/plugins/multi_region_endpoint.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + + +module Aws + module EventBridge + module Plugins + # Resolve Multi-Region Endpoints + class MultiRegionEndpoint < Seahorse::Client::Plugin + + def add_handlers(handlers, _config) + handlers.add(Handler, operations: [:put_events]) + end + + # After extracting out any ARN input, resolve a new URL with it. + class Handler < Seahorse::Client::Handler + def call(context) + if (multi_region_endpoint_id = context.params[:endpoint_id]) + + validate_multi_region_endpoint!(multi_region_endpoint_id) + validate_config!(context) + + url = context.http_request.endpoint + region = context.config.region + + # if regional_endpoint is false, a custom endpoint was provided + # customer provided endpoints should be used as is + if context.config.regional_endpoint + sfx = Aws::Partitions::EndpointProvider.dns_suffix_for( + region, 'events', { dualstack: context.config.use_dualstack_endpoint } + ) + url.host = "#{multi_region_endpoint_id}.endpoint.events.#{sfx}" + end + + context.config.sigv4_signer = sigv4a_signer(context) + end + + @handler.call(context) + end + + private + + def validate_multi_region_endpoint!(multi_region_endpoint_id) + unless !multi_region_endpoint_id.empty? && + valid_hostname?(multi_region_endpoint_id) + raise ArgumentError, 'multi_region_endpoint_id must be a valid host label.' + end + end + + def valid_hostname?(str) + str =~ /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$/ + end + + def validate_config!(context) + if context.config.use_fips_endpoint + raise ArgumentError, + 'FIPS is not supported with EventBridge multi-region endpoints.' + end + end + + def sigv4a_signer(context) + existing = context.config.sigv4_signer + Aws::Sigv4::Signer.new( + service: existing.service, + region: '*', + credentials_provider: existing.credentials_provider, + signing_algorithm: :sigv4a, + uri_escape_path: true, + unsigned_headers: existing.unsigned_headers + ) + end + end + end + end + end +end diff --git a/gems/aws-sdk-eventbridge/spec/plugins/multi_region_endpoint_spec.rb b/gems/aws-sdk-eventbridge/spec/plugins/multi_region_endpoint_spec.rb new file mode 100644 index 00000000000..0142e53496d --- /dev/null +++ b/gems/aws-sdk-eventbridge/spec/plugins/multi_region_endpoint_spec.rb @@ -0,0 +1,135 @@ +require_relative '../spec_helper' + +module Aws + module EventBridge + describe Client do + let(:region) { 'us-east-1' } + let(:use_fips_endpoint) { false } + let(:use_dualstack_endpoint) { false } + let(:client) do + Client.new( + stub_responses: true, region: region, + use_dualstack_endpoint: use_dualstack_endpoint, + use_fips_endpoint: use_fips_endpoint + ) + end + let(:event) do + { + time: Time.now, + source: "source", + } + end + let(:entries) { [event] } + + def expect_sigv4a_signer(region='*') + mock_signature = Aws::Sigv4::Signature.new(headers: {}) + mock_signer = double('sigv4a_signer', sign_request: mock_signature) + + # a base signer is always created + # multi-region endpoints result in a second signer being created with :sigv4a + allow(Aws::Sigv4::Signer).to receive(:new).and_call_original + allow(Aws::Sigv4::Signer).to receive(:new).with(hash_including(region: region, signing_algorithm: :sigv4a)).and_return(mock_signer) + end + + it 'does not update the endpoint when endpoint_id is not set' do + resp = client.put_events(entries: entries) + expect(resp.context.http_request.endpoint.host).to eq('events.us-east-1.amazonaws.com') + end + + it 'it updates the endpoint, signs with sigv4a and uses the global region when endpoint_id is set ' do + expect_sigv4a_signer('*') + resp = client.put_events(entries: entries, endpoint_id: 'abc123.456def') + expect(resp.context.http_request.endpoint.host).to eq('abc123.456def.endpoint.events.amazonaws.com') + end + + it 'raises when given an invalid endpoint_id' do + expect do + client.put_events(entries: entries, endpoint_id: 'badactor.com?foo=bar') + end.to raise_error(ArgumentError) + end + + it 'raises when given an empty endpoint_id' do + expect do + client.put_events(entries: entries, endpoint_id: '') + end.to raise_error(ArgumentError) + end + + context 'use_dualstack_endpoint' do + let(:use_dualstack_endpoint) { true } + + it 'does not update the endpoint when endpoint_id is not set' do + resp = client.put_events(entries: entries) + expect(resp.context.http_request.endpoint.host).to eq('events.us-east-1.api.aws') + end + + it 'uses the dualstack dnsSuffix when endpoint_id is set' do + expect_sigv4a_signer('*') + resp = client.put_events(entries: entries, endpoint_id: 'abc123.456def') + expect(resp.context.http_request.endpoint.host).to eq('abc123.456def.endpoint.events.api.aws') + end + end + + context 'use_fips_endpoint' do + let(:use_fips_endpoint) { true } + + it 'does not update the endpoint when endpoint_id is not set' do + resp = client.put_events(entries: entries) + expect(resp.context.http_request.endpoint.host).to eq('events-fips.us-east-1.amazonaws.com') + end + + it 'raises when endpoint_id and use_fips_endpoint are set' do + expect do + client.put_events(entries: entries, endpoint_id: 'abc123.456def') + end.to raise_error(ArgumentError) + end + end + + context 'use_fips_endpoint and use_dualstack_endpoint' do + let(:use_fips_endpoint) { true } + let(:use_dualstack_endpoint) { true } + + it 'does not update the endpoint when endpoint_id is not set' do + resp = client.put_events(entries: entries) + expect(resp.context.http_request.endpoint.host).to eq('events-fips.us-east-1.api.aws') + end + + it 'raises when endpoint_id and use_fips_endpoint are set' do + expect do + client.put_events(entries: entries, endpoint_id: 'abc123.456def') + end.to raise_error(ArgumentError) + end + end + + context 'aws-iso partition' do + let(:region) { 'us-iso-east-1' } + + it 'does not update the endpoint when endpoint_id is not set' do + resp = client.put_events(entries: entries) + expect(resp.context.http_request.endpoint.host).to eq('events.us-iso-east-1.c2s.ic.gov') + end + + it 'it updates the endpoint, signs with sigv4a and uses the global region when endpoint_id is set ' do + expect_sigv4a_signer('*') + resp = client.put_events(entries: entries, endpoint_id: 'abc123.456def') + expect(resp.context.http_request.endpoint.host).to eq('abc123.456def.endpoint.events.c2s.ic.gov') + end + end + + context 'custom endpoint' do + let(:custom_endpoint) { 'https://example.org' } + let(:client) { Client.new(endpoint: custom_endpoint, stub_responses: true, region: region) } + + it 'uses the custom endpoint when endpoint_id is not set' do + resp = client.put_events(entries: entries) + expect(resp.context.http_request.endpoint.host).to eq('example.org') + end + + it 'does not modify the custom endpoint when endpoint_id is set' do + expect_sigv4a_signer('*') + resp = client.put_events(entries: entries, endpoint_id: 'abc123.456def') + expect(resp.context.http_request.endpoint.host).to eq('example.org') + end + end + end + end +end diff --git a/services.json b/services.json index ddebcf3bc5a..4a6fe9d1a1f 100644 --- a/services.json +++ b/services.json @@ -362,7 +362,10 @@ "models": "es/2015-01-01" }, "EventBridge": { - "models": "eventbridge/2015-10-07" + "models": "eventbridge/2015-10-07", + "addPlugins": [ + "Aws::EventBridge::Plugins::MultiRegionEndpoint" + ] }, "FIS": { "models": "fis/2020-12-01"