Skip to content

Commit cf9cdf7

Browse files
authored
Add presigned request to S3 object (#2613)
1 parent 4e50ef4 commit cf9cdf7

File tree

3 files changed

+168
-1
lines changed

3 files changed

+168
-1
lines changed

gems/aws-sdk-s3/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Unreleased Changes
22
------------------
33

4+
* Feature - Add `presigned_request` method to `Aws::S3::Object`.
5+
46
1.105.1 (2021-11-05)
57
------------------
68

gems/aws-sdk-s3/lib/aws-sdk-s3/customizations/object.rb

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def presigned_post(options = {})
161161
#
162162
# @param [Symbol] method
163163
# The S3 operation to generate a presigned URL for. Valid values
164-
# are `:get`, `:put`, `:head`, `:delete`, `:create_multipart_upload`,
164+
# are `:get`, `:put`, `:head`, `:delete`, `:create_multipart_upload`,
165165
# `:list_multipart_uploads`, `:complete_multipart_upload`,
166166
# `:abort_multipart_upload`, `:list_parts`, and `:upload_part`.
167167
#
@@ -215,6 +215,79 @@ def presigned_url(method, params = {})
215215
)
216216
end
217217

218+
# Allows you to create presigned URL requests for S3 operations. This
219+
# method returns a tuple containing the URL and the signed X-amz-* headers
220+
# to be used with the presigned url.
221+
#
222+
# @example Pre-signed GET URL, valid for one hour
223+
#
224+
# obj.presigned_request(:get, expires_in: 3600)
225+
# #=> ["https://bucket-name.s3.amazonaws.com/object-key?...", {}]
226+
#
227+
# @example Pre-signed PUT with a canned ACL
228+
#
229+
# # the object uploaded using this URL will be publicly accessible
230+
# obj.presigned_request(:put, acl: 'public-read')
231+
# #=> ["https://bucket-name.s3.amazonaws.com/object-key?...",
232+
# {"x-amz-acl"=>"public-read"}]
233+
#
234+
# @param [Symbol] method
235+
# The S3 operation to generate a presigned request for. Valid values
236+
# are `:get`, `:put`, `:head`, `:delete`, `:create_multipart_upload`,
237+
# `:list_multipart_uploads`, `:complete_multipart_upload`,
238+
# `:abort_multipart_upload`, `:list_parts`, and `:upload_part`.
239+
#
240+
# @param [Hash] params
241+
# Additional request parameters to use when generating the pre-signed
242+
# request. See the related documentation in {Client} for accepted
243+
# params.
244+
#
245+
# | Method | Client Method |
246+
# |------------------------------|------------------------------------|
247+
# | `:get` | {Client#get_object} |
248+
# | `:put` | {Client#put_object} |
249+
# | `:head` | {Client#head_object} |
250+
# | `:delete` | {Client#delete_object} |
251+
# | `:create_multipart_upload` | {Client#create_multipart_upload} |
252+
# | `:list_multipart_uploads` | {Client#list_multipart_uploads} |
253+
# | `:complete_multipart_upload` | {Client#complete_multipart_upload} |
254+
# | `:abort_multipart_upload` | {Client#abort_multipart_upload} |
255+
# | `:list_parts` | {Client#list_parts} |
256+
# | `:upload_part` | {Client#upload_part} |
257+
#
258+
# @option params [Boolean] :virtual_host (false) When `true` the
259+
# presigned URL will use the bucket name as a virtual host.
260+
#
261+
# bucket = Aws::S3::Bucket.new('my.bucket.com')
262+
# bucket.object('key').presigned_request(virtual_host: true)
263+
# #=> ["http://my.bucket.com/key?...", {}]
264+
#
265+
# @option params [Integer] :expires_in (900) Number of seconds before
266+
# the pre-signed URL expires. This may not exceed one week (604800
267+
# seconds). Note that the pre-signed URL is also only valid as long as
268+
# credentials used to sign it are. For example, when using IAM roles,
269+
# temporary tokens generated for signing also have a default expiration
270+
# which will affect the effective expiration of the pre-signed URL.
271+
#
272+
# @raise [ArgumentError] Raised if `:expires_in` exceeds one week
273+
# (604800 seconds).
274+
#
275+
# @return [String, Hash] A tuple with a presigned URL and headers that
276+
# should be included with the request.
277+
#
278+
def presigned_request(method, params = {})
279+
presigner = Presigner.new(client: client)
280+
281+
if %w(delete head get put).include?(method.to_s)
282+
method = "#{method}_object".to_sym
283+
end
284+
285+
presigner.presigned_request(
286+
method.downcase,
287+
params.merge(bucket: bucket_name, key: key)
288+
)
289+
end
290+
218291
# Returns the public (un-signed) URL for this object.
219292
#
220293
# s3.bucket('bucket-name').object('obj-key').public_url

gems/aws-sdk-s3/spec/object/presigned_url_spec.rb

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,98 @@ module S3
103103
end.to raise_error(ArgumentError, /:key must not be blank/)
104104
end
105105
end
106+
107+
describe '#presigned_request' do
108+
context 'method name automatic suffix' do
109+
let(:presigner) do
110+
double("DummyPresigner",
111+
presigned_request: ['some url', {"some" => "header"}])
112+
end
113+
114+
before do
115+
allow(Aws::S3::Presigner).to receive(:new).and_return(presigner)
116+
end
117+
118+
method_expectations = {
119+
# HTTP Methods
120+
delete: :delete_object,
121+
head: :head_object,
122+
get: :get_object,
123+
put: :put_object,
124+
# Non-HTTP methods
125+
create_multipart_upload: :create_multipart_upload,
126+
list_multipart_uploads: :list_multipart_uploads,
127+
complete_multipart_upload: :complete_multipart_upload,
128+
abort_multipart_upload: :abort_multipart_upload,
129+
list_parts: :list_parts,
130+
upload_part: :upload_part
131+
}
132+
133+
method_expectations.each do |method, expected_method|
134+
it "rewrites #{method} as #{expected_method} to the Presigner" do
135+
obj = Object.new(
136+
bucket_name: bucket_name,
137+
key: key,
138+
client: client
139+
)
140+
141+
expect(presigner).to receive(:presigned_request).with(
142+
expected_method,
143+
bucket: bucket_name,
144+
key: key
145+
)
146+
147+
obj.presigned_request(method)
148+
end
149+
end
150+
end
151+
152+
# from http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
153+
it 'generates a valid presigned request' do
154+
obj = Object.new(
155+
bucket_name: 'examplebucket',
156+
key: 'test.txt',
157+
client: client
158+
)
159+
160+
now = Time.parse('20130524T000000Z')
161+
allow(Time).to receive(:now).and_return(now)
162+
url, headers = obj.presigned_request(
163+
:get, expires_in: 86_400, request_payer: 'peccy')
164+
expect(url).to eq(
165+
'https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm='\
166+
'AWS4-HMAC-SHA256&X-Amz-Credential=ACCESS_KEY_ID%2F20130524%2F'\
167+
'us-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z'\
168+
'&X-Amz-Expires=86400&X-Amz-SignedHeaders=host%3B'\
169+
'x-amz-request-payer&X-Amz-Signature='\
170+
'7adcfb1b638c3198eca2c9d4637394e2d90ffe2bcc717056ef8e5eb4d73946b2'
171+
)
172+
expect(headers).to eq({"x-amz-request-payer" => "peccy"})
173+
end
174+
175+
it 'can use the bucket name to create a virtual hosted url' do
176+
obj = Object.new(
177+
bucket_name: 'my.bucket.com',
178+
key: 'test.txt',
179+
client: client
180+
)
181+
182+
url, _headers = obj.presigned_request(:get, virtual_host: true)
183+
expect(url).to match(%r{^https://my\.bucket\.com(:443)?/})
184+
end
185+
186+
it 'rejects empty keys' do
187+
obj = Object.new(
188+
'bucket-name',
189+
'',
190+
client: client
191+
)
192+
expect(obj.key).to eq('')
193+
expect do
194+
obj.presigned_request(:get)
195+
end.to raise_error(ArgumentError, /:key must not be blank/)
196+
end
197+
end
106198
end
107199
end
108200
end

0 commit comments

Comments
 (0)