gems/aws-sdk-s3/spec/presigner_spec.rb (436 lines of code) (raw):
# frozen_string_literal: true
require_relative 'spec_helper'
module Aws
module S3
describe Presigner do
let(:client) { Aws::S3::Client.new(**client_opts) }
let(:credentials) do
Credentials.new(
'ACCESS_KEY_ID',
'SECRET_ACCESS_KEY'
)
end
let(:client_opts) do
{
region: 'us-east-1',
credentials: credentials,
stub_responses: true
}
end
subject { Presigner.new(client: client) }
let(:time) { Time.utc(2021, 8, 27) }
before { allow(Time).to receive(:now).and_return(time) }
let(:expiration_time) { time + 180 }
let(:credentials_provider_class) do
Class.new do
include CredentialProvider
def initialize(expiration_time)
@credentials = Credentials.new(
'akid',
'secret',
'session'
)
@expiration = expiration_time
end
end
end
describe '#initialize' do
it 'accepts an injected S3 client' do
pre = Presigner.new(client: client)
expect(pre.class).to eq(Aws::S3::Presigner)
end
it 'can be constructed without a client' do
expect(Aws::S3::Client).to receive(:new).and_return(client)
pre = Presigner.new
expect(pre.class).to eq(Aws::S3::Presigner)
end
end
describe '#presigned_url' do
it 'will be tracked as an api request' do
subject.presigned_url(:get_object, bucket: 'bkt', key: 'k')
expect(client.api_requests.size).to eq(1)
end
it 'labels context as a presigned request before handlers are invoked' do
expect do |block|
client.handle_request(&block)
subject.presigned_url(:get_object, bucket: 'bkt', key: 'k')
end.to yield_with_args { |c| c[:presigned_url] == true }
end
it 'can be excluded from being tracked as an api request' do
subject.presigned_url(:get_object, bucket: 'bkt', key: 'k')
expect(client.api_requests(exclude_presign: true)).to be_empty
end
it 'can presign #get_object to spec' do
expected_url = 'https://examplebucket.s3.amazonaws.com/test.txt?'\
'X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential='\
'ACCESS_KEY_ID%2F20210827%2Fus-east-1%2Fs3%2F'\
'aws4_request&X-Amz-Date=20210827T000000Z'\
'&X-Amz-Expires=86400&X-Amz-SignedHeaders=host'\
'&X-Amz-Signature=cd4953fc4c1ebb97c3ca18ce433b4bc9ff9'\
'f9f6a54eb47c31d908e0e7ecf524c'
actual_url = subject.presigned_url(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: 86_400
)
expect(CGI.parse(actual_url)).to eq(CGI.parse(expected_url))
end
it 'can sign with a given time' do
actual_url = subject.presigned_url(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: 86_400,
time: Time.utc(2022, 02, 22)
)
expect(actual_url).to include('&X-Amz-Date=20220222T000000Z')
end
it 'can sign with additional whitelisted headers' do
actual_url = subject.presigned_url(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: 86_400,
whitelist_headers: ['user-agent']
)
expect(actual_url).to include(
'&X-Amz-SignedHeaders=host%3Buser-agent'
)
end
it 'raises when expires_in length is over 1 week' do
expect do
subject.presigned_url(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: (7 * 86_400) + 1
)
end.to raise_error(ArgumentError)
end
it 'raises when expires_in is less than or equal to 0' do
expect do
subject.presigned_url(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: 0
)
end.to raise_error(ArgumentError)
end
it 'can generate http (non-secure) urls' do
url = subject.presigned_url(
:get_object,
bucket: 'aws-sdk',
key: 'foo',
secure: false
)
expect(url).to match(/^http:/)
end
it 'uses the configured :endpoint scheme' do
client_opts[:endpoint] = 'http://example.com'
client_opts[:force_path_style] = true
url = subject.presigned_url(
:get_object,
bucket: 'aws-sdk',
key: 'foo'
)
expect(url).to start_with('http://example.com')
end
it 'uses the configured :endpoint port' do
client_opts[:endpoint] = 'http://localhost:9000'
client_opts[:force_path_style] = true
url = subject.presigned_url(
:get_object,
bucket: 'aws-sdk',
key: 'foo'
)
expect(url).to start_with('http://localhost:9000')
end
it 'supports virtual hosting' do
url = subject.presigned_url(
:get_object,
bucket: 'my.website.com',
key: 'foo',
virtual_host: true
)
expect(url).to match(/^https:\/\/my.website.com\/foo/)
end
it 'hoists x-amz-* headers to the query string' do
url = subject.presigned_url(
:put_object,
bucket: 'aws-sdk',
key: 'foo',
acl: 'public-read'
)
expect(url).to match(/x-amz-acl=public-read/)
end
it 'does not normalize object keys' do
url = subject.presigned_url(
:get_object,
bucket: 'aws-sdk',
key: 'foo/../bar'
)
expect(url).to include('foo/../bar')
end
context 'credential expiration' do
let(:credentials) do
credentials_provider_class.new(expiration_time)
end
it 'picks the minimum time between expires_in and credential expiration' do
url = subject.presigned_url(
:get_object,
bucket: 'aws-sdk',
key: 'foo',
expires_in: 3600
)
expect(url).to match(/X-Amz-Expires=180/)
end
end
end
describe '#presigned_request' do
it 'will be tracked as an api request' do
subject.presigned_request(:get_object, bucket: 'bkt', key: 'k')
expect(client.api_requests.size).to eq(1)
end
it 'labels context as a presigned request before handlers are invoked' do
expect do |block|
client.handle_request(&block)
subject.presigned_request(:get_object, bucket: 'bkt', key: 'k')
end.to yield_with_args { |c| c[:presigned_url] == true }
end
it 'can be excluded from being tracked as an api request' do
subject.presigned_request(:get_object, bucket: 'bkt', key: 'k')
expect(client.api_requests(exclude_presign: true)).to be_empty
end
it 'can presign #get_object to spec' do
expected_url = 'https://examplebucket.s3.amazonaws.com/test.txt?'\
'X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential='\
'ACCESS_KEY_ID%2F20210827%2Fus-east-1%2Fs3%2F'\
'aws4_request&X-Amz-Date=20210827T000000Z'\
'&X-Amz-Expires=86400&X-Amz-SignedHeaders=host'\
'&X-Amz-Signature=cd4953fc4c1ebb97c3ca18ce433b4bc9ff9'\
'f9f6a54eb47c31d908e0e7ecf524c'
actual_url, = subject.presigned_request(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: 86_400
)
expect(CGI.parse(actual_url)).to eq(CGI.parse(expected_url))
end
it 'can sign with a given time' do
actual_url, = subject.presigned_request(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: 86_400,
time: Time.utc(2022, 02, 22)
)
expect(actual_url).to include('&X-Amz-Date=20220222T000000Z')
end
it 'can sign with additional whitelisted headers' do
actual_url, = subject.presigned_request(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: 86_400,
whitelist_headers: ['user-agent']
)
expect(actual_url).to include(
'&X-Amz-SignedHeaders=host%3Buser-agent'
)
end
it 'raises when expires_in length is over 1 week' do
expect do
subject.presigned_request(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: (7 * 86_400) + 1
)
end.to raise_error(ArgumentError)
end
it 'raises when expires_in is less than or equal to 0' do
expect do
subject.presigned_request(
:get_object,
bucket: 'examplebucket',
key: 'test.txt',
expires_in: 0
)
end.to raise_error(ArgumentError)
end
it 'can generate http (non-secure) urls' do
url, = subject.presigned_request(
:get_object,
bucket: 'aws-sdk',
key: 'foo',
secure: false
)
expect(url).to match(/^http:/)
end
it 'uses the configured :endpoint scheme' do
client_opts[:endpoint] = 'http://example.com'
client_opts[:force_path_style] = true
url, = subject.presigned_request(
:get_object,
bucket: 'aws-sdk',
key: 'foo'
)
expect(url).to start_with('http://example.com')
end
it 'uses the configured :endpoint port' do
client_opts[:endpoint] = 'http://localhost:9000'
client_opts[:force_path_style] = true
url, = subject.presigned_request(
:get_object,
bucket: 'aws-sdk',
key: 'foo'
)
expect(url).to start_with('http://localhost:9000')
end
it 'supports virtual hosting' do
url, = subject.presigned_request(
:get_object,
bucket: 'my.website.com',
key: 'foo',
virtual_host: true
)
expect(url).to match(/^https:\/\/my.website.com\/foo/)
end
it 'returns x-amz-* headers instead of hoisting to the query string' do
url, headers = subject.presigned_request(
:put_object, bucket: 'aws-sdk', key: 'foo', acl: 'public-read'
)
expect(url).to match(/X-Amz-SignedHeaders=host%3Bx-amz-acl/)
expect(headers).to eq('x-amz-acl' => 'public-read')
end
it 'does not normalize object keys' do
url, = subject.presigned_request(
:get_object,
bucket: 'aws-sdk',
key: 'foo/../bar'
)
expect(url).to include('foo/../bar')
end
context 'credential expiration' do
let(:credentials) do
credentials_provider_class.new(expiration_time)
end
it 'picks the minimum time between expires_in and credential expiration' do
url, = subject.presigned_request(
:get_object,
bucket: 'aws-sdk',
key: 'foo',
expires_in: 3600
)
expect(url).to match(/X-Amz-Expires=180/)
end
end
end
context 'outpost access point ARNs' do
it 'uses s3-outposts as the service' do
arn = 'arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint'
url = subject.presigned_url(:get_object, bucket: arn, key: 'obj')
expected_service = 's3-outposts'
expect(url).to include('X-Amz-Signature')
expect(url).to include("#{expected_service}%2Faws4_request")
end
it 'uses the resolved-region' do
arn_region = 'us-east-1'
arn = "arn:aws:s3-outposts:#{arn_region}:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"
url = subject.presigned_url(:get_object, bucket: arn, key: 'obj')
expect(url).to include('X-Amz-Signature')
expect(url).to include("s3-outposts.#{arn_region}.")
end
end
context 'access point ARN' do
it 'uses s3 as the service' do
arn = 'arn:aws:s3:us-west-2:123456789012:accesspoint/myendpoint'
url = subject.presigned_url(:get_object, bucket: arn, key: 'obj')
expected_service = 's3'
expect(url).to include(
"20210827%2Fus-west-2%2F#{expected_service}%2Faws4_request"
)
expect(url).to include(
'd6b2a8840209fa40456c97ae99f9fab2526316d70f3ebaa75c22d654b90e9da9'
)
end
it 'uses the resolved-region' do
arn_region = 'us-east-1'
arn = "arn:aws:s3:#{arn_region}:123456789012:accesspoint/myendpoint"
url = subject.presigned_url(:get_object, bucket: arn, key: 'obj')
expect(url).to include(
"20210827%2F#{arn_region}%2Fs3%2Faws4_request"
)
expect(url).to include(
'5a27899693cea5f6ccf9dc26a3e44c4a3d45ae57a441954fb4b7cdc8c2ef45ea'
)
end
end
context 'MRAP ARNs' do
let(:signer) { double('sigv4a_signer') }
let(:arn) { 'arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap' }
it 'creates a presigned url with sigv4a' do
stub_const(
'Aws::Plugins::Sign::SUPPORTED_AUTH_TYPES',
Aws::Plugins::Sign::SUPPORTED_AUTH_TYPES + ['sigv4a']
)
expect(Aws::Sigv4::Signer)
.to receive(:new)
.with(hash_including(
service: 's3',
region: '*',
signing_algorithm: :sigv4a
))
.and_return(signer)
expect(signer)
.to receive(:presign_url)
.with(hash_including(
url: URI.parse('https://mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com/obj')
))
subject.presigned_url(:get_object, bucket: arn, key: 'obj')
end
context 's3_disable_multiregion_access_points is true' do
let(:client) do
Aws::S3::Client.new(
stub_responses: true,
s3_disable_multiregion_access_points: true
)
end
it 'raises an ArgumentError' do
arn = 'arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap'
expect do
subject.presigned_url(:get_object, bucket: arn, key: 'obj')
end.to raise_error(ArgumentError)
end
end
end
context 'express endpoints' do
it 'uses express credentials' do
bucket = 'presign-bucket--use1-az2--x-s3'
credentials = {
access_key_id: 's3-akid',
secret_access_key: 's3-secret',
session_token: 's3-session',
expiration: Time.now + 60 * 5
}
client.stub_responses(:create_session, credentials: credentials)
expect(client).to receive(:create_session)
.with({bucket: bucket}).and_call_original
url = subject.presigned_url(:get_object, bucket: bucket, key: 'obj')
expect(url).to include('X-Amz-Credential=s3-akid')
expect(url).to include('s3express%2Faws4_request')
expect(url).to include('X-Amz-S3session-Token=s3-session')
end
it 'does not use express credentials when disabled' do
client_opts[:disable_s3_express_session_auth] = true
bucket = 'presign-bucket--use1-az2--x-s3'
expect(client).not_to receive(:create_session)
url = subject.presigned_url(:get_object, bucket: bucket, key: 'obj')
expect(url).not_to include('X-Amz-S3session-Token')
end
end
end
end
end