module Aws module S3 module Plugins # When an accesspoint ARN is provided for :bucket in S3 operations, this # plugin resolves the request endpoint from the ARN when possible. class BucketARN < Seahorse::Client::Plugin option( :s3_use_arn_region, default: true, doc_type: 'Boolean', docstring: <<-DOCS) do |cfg| By default, the SDK will use the S3 ARN region, and cross-region requests could be made. Set to `false` to not use the region from the S3 ARN. DOCS resolve_s3_use_arn_region(cfg) end def add_handlers(handlers, _config) handlers.add(Handler) end # @api private class Handler < Seahorse::Client::Handler def call(context) bucket_member = _bucket_member(context.operation.input.shape) if bucket_member && (bucket = context.params[bucket_member]) _resolved_bucket, _resolved_region, arn = BucketARN.resolve_arn!( bucket, context.config.region, context.config.s3_use_arn_region ) if arn if arn.resource.start_with?('accesspoint') validate_config!(context.config) end dualstack = extract_dualstack_config!(context) BucketARN.resolve_url!( context.http_request.endpoint, arn, dualstack ) end end @handler.call(context) end private def _bucket_member(input) input.members.each do |member, ref| return member if ref.shape.name == 'BucketName' end nil end # other plugins use dualstack so disable it when we're done def extract_dualstack_config!(context) dualstack = context[:use_dualstack_endpoint] context[:use_dualstack_endpoint] = false if dualstack dualstack end def validate_config!(config) unless config.regional_endpoint raise ArgumentError, 'Cannot provide both an accesspoint ARN and :endpoint.' end if config.use_accelerate_endpoint raise ArgumentError, 'Cannot provide both an accesspoint ARN and setting '\ ':use_accelerate_endpoint to true.' end if config.force_path_style raise ArgumentError, 'Cannot provide both an accesspoint ARN and setting '\ ':force_path_style to true.' end end end class << self # @api private def resolve_arn!(bucket_name, region, s3_use_arn_region) if Aws::ARNParser.arn?(bucket_name) arn = Aws::ARNParser.parse(bucket_name) validate_s3_arn!(arn) validate_region!(arn, region, s3_use_arn_region) if arn.resource.start_with?('accesspoint') region = arn.region if s3_use_arn_region [bucket_name, region, arn] else raise ArgumentError, 'Only accesspoint type ARNs are currently supported.' end else [bucket_name, region] end end # @api private def resolve_url!(url, arn, dualstack = false) if arn.resource.start_with?('accesspoint') url.host = accesspoint_arn_host(arn, dualstack) else raise ArgumentError, 'Only accesspoint type ARNs are currently supported.' end url.path = url_path(url.path, arn) url end private def accesspoint_arn_host(arn, dualstack) _resource_type, resource_name = parse_resource(arn.resource) accesspoint = "#{resource_name}-#{arn.account_id}" accesspoint << '.s3-accesspoint' accesspoint << '.dualstack' if dualstack sfx = Aws::Partitions::EndpointProvider.dns_suffix_for(arn.region) accesspoint << ".#{arn.region}.#{sfx}" accesspoint end def parse_resource(str) slash = str.index('/') || str.length colon = str.index(':') || str.length delimiter = slash < colon ? slash : colon if delimiter < str.length [str[0..(delimiter - 1)], str[(delimiter + 1)..-1]] else [nil, str] end end def resolve_s3_use_arn_region(cfg) value = ENV['AWS_S3_USE_ARN_REGION'] || Aws.shared_config.s3_use_arn_region(profile: cfg.profile) || 'true' # Raise if provided value is not true or false if value != 'true' && value != 'false' raise ArgumentError, 'Must provide either `true` or `false` for '\ 's3_use_arn_region profile option or for '\ 'ENV[\'AWS_S3_USE_ARN_REGION\']' end value == 'true' end def url_path(path, arn) path = path.sub("/#{Seahorse::Util.uri_escape(arn.to_s)}", '') .sub("/#{arn}", '') "/#{path}" unless path.match(/^\//) path end def validate_s3_arn!(arn) _resource_type, resource_name = parse_resource(arn.resource) unless arn.service == 's3' raise ArgumentError, 'Must provide an S3 ARN.' end if arn.region.empty? || arn.account_id.empty? raise ArgumentError, 'S3 Access Point ARNs must contain both a valid region '\ ' and a valid account id.' end if resource_name.include?(':') || resource_name.include?('/') raise ArgumentError, 'ARN resource id must be a single value.' end unless Plugins::BucketDns.valid_subdomain?( "#{resource_name}-#{arn.account_id}" ) raise ArgumentError, "#{resource_name}-#{arn.account_id} is not a "\ 'valid subdomain.' end end def validate_region!(arn, region, s3_use_arn_region) if region.include?('fips') raise ArgumentError, 'FIPS client regions are currently not supported with '\ 'accesspoint ARNs.' end if s3_use_arn_region && !Aws::Partitions.partition(arn.partition).region?(region) raise Aws::Errors::InvalidARNPartitionError end if !s3_use_arn_region && region != arn.region raise Aws::Errors::InvalidARNRegionError end end end end end end end