Skip to content

Commit d313084

Browse files
authored
Add more operations to Access Grants (#3069)
1 parent 10452d6 commit d313084

File tree

4 files changed

+134
-7
lines changed

4 files changed

+134
-7
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 - Support `head_bucket`, `get_object_attributes`, `delete_objects`, and `copy_object` for Access Grants.
5+
46
1.156.0 (2024-07-02)
57
------------------
68

gems/aws-sdk-s3/lib/aws-sdk-s3/access_grants_credentials_provider.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ def initialize(options = {})
4747
@caching = options.delete(:caching) != false
4848
@s3_control_clients = {}
4949
@bucket_region_cache = Aws::S3.bucket_region_cache
50+
@head_bucket_mutex = Mutex.new
51+
@head_bucket_call = false
5052
return unless @caching
5153

5254
@credentials_cache = Aws::S3.access_grants_credentials_cache
@@ -195,9 +197,16 @@ def cached_bucket_region_for(bucket)
195197
end
196198

197199
def new_bucket_region_for(bucket)
198-
@s3_client.head_bucket(bucket: bucket).bucket_region
199-
rescue Aws::S3::Errors::Http301Error => e
200-
e.data.region
200+
@head_bucket_mutex.synchronize do
201+
begin
202+
@head_bucket_call = true
203+
@s3_client.head_bucket(bucket: bucket).bucket_region
204+
rescue Aws::S3::Errors::Http301Error => e
205+
e.data.region
206+
ensure
207+
@head_bucket_call = false
208+
end
209+
end
201210
end
202211

203212
# returns the account id for the configured credentials

gems/aws-sdk-s3/lib/aws-sdk-s3/plugins/access_grants.rb

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,25 +44,47 @@ class Handler < Seahorse::Client::Handler
4444
list_objects_v2: 'READ',
4545
list_object_versions: 'READ',
4646
list_parts: 'READ',
47+
head_bucket: 'READ',
48+
get_object_attributes: 'READ',
4749
put_object: 'WRITE',
4850
put_object_acl: 'WRITE',
4951
delete_object: 'WRITE',
5052
abort_multipart_upload: 'WRITE',
5153
create_multipart_upload: 'WRITE',
5254
upload_part: 'WRITE',
53-
complete_multipart_upload: 'WRITE'
55+
complete_multipart_upload: 'WRITE',
56+
delete_objects: 'WRITE',
57+
copy_object: 'READWRITE'
5458
}.freeze
5559

5660
def call(context)
61+
provider = context.config.access_grants_credentials_provider
62+
5763
if access_grants_operation?(context) &&
58-
!s3_express_endpoint?(context)
64+
!s3_express_endpoint?(context) &&
65+
!credentials_head_bucket_call?(provider)
5966
params = context[:endpoint_params]
6067
permission = PERMISSION_MAP[context.operation_name]
6168

62-
provider = context.config.access_grants_credentials_provider
69+
key =
70+
case context.operation_name
71+
when :delete_objects
72+
delete_params = context.params[:delete]
73+
common_prefixes(delete_params[:objects].map { |o| o[:key] })
74+
when :copy_object
75+
source_bucket, source_key = params[:copy_source].split('/', 2)
76+
if params[:bucket] != source_bucket
77+
raise ArgumentError,
78+
'source and destination bucket must be the same'
79+
end
80+
common_prefixes([params[:key], source_key])
81+
else
82+
params[:key]
83+
end
84+
6385
credentials = provider.access_grants_credentials_for(
6486
bucket: params[:bucket],
65-
key: params[:key],
87+
key: key,
6688
prefix: params[:prefix],
6789
permission: permission
6890
)
@@ -80,6 +102,12 @@ def with_metric(credentials, &block)
80102
Aws::Plugins::UserAgent.metric('S3_ACCESS_GRANTS', &block)
81103
end
82104

105+
# HeadBucket is a supported call. When fetching credentials,
106+
# this plugin is executed again, and becomes recursive.
107+
def credentials_head_bucket_call?(provider)
108+
provider.instance_variable_get(:@head_bucket_call)
109+
end
110+
83111
def access_grants_operation?(context)
84112
params = context[:endpoint_params]
85113
params[:bucket] && PERMISSION_MAP[context.operation_name]
@@ -88,6 +116,42 @@ def access_grants_operation?(context)
88116
def s3_express_endpoint?(context)
89117
context[:endpoint_properties]['backend'] == 'S3Express'
90118
end
119+
120+
# Return the common prefix of the keys, regardless of the delimiter.
121+
# For example, given keys ['foo/bar', 'foo/baz'], the common prefix
122+
# is 'foo/ba'.
123+
def common_prefixes(keys)
124+
return '' if keys.empty?
125+
126+
first_key = keys[0]
127+
common_ancestor = first_key
128+
last_prefix = ''
129+
keys.each do |k|
130+
until common_ancestor.empty?
131+
break if k.start_with?(common_ancestor)
132+
133+
last_index = common_ancestor.rindex('/')
134+
return '' if last_index.nil?
135+
136+
last_prefix = common_ancestor[(last_index + 1)..-1]
137+
common_ancestor = common_ancestor[0...last_index]
138+
end
139+
end
140+
new_common_ancestor = "#{common_ancestor}/#{last_prefix}"
141+
keys.each do |k|
142+
until last_prefix.empty?
143+
break if k.start_with?(new_common_ancestor)
144+
145+
last_prefix = last_prefix[0...-1]
146+
new_common_ancestor = "#{common_ancestor}/#{last_prefix}"
147+
end
148+
end
149+
if new_common_ancestor == "#{first_key}/"
150+
first_key
151+
else
152+
new_common_ancestor
153+
end
154+
end
91155
end
92156

93157
def add_handlers(handlers, config)

gems/aws-sdk-s3/spec/plugins/access_grants_spec.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,58 @@ module S3
108108
expect(provider.s3_client).to be_nil
109109
end
110110
end
111+
112+
context 'delete_objects' do
113+
it 'key with no common ancestor' do
114+
keys = %w[A/log.txt B/log.txt C/log.txt]
115+
expect_any_instance_of(Aws::S3::AccessGrantsCredentialsProvider)
116+
.to receive(:access_grants_credentials_for)
117+
.with(bucket: 'bucket', key: '', permission: 'WRITE', prefix: nil)
118+
delete = { objects: keys.map { |key| { key: key } } }
119+
client.delete_objects(bucket: 'bucket', delete: delete)
120+
end
121+
122+
it 'key with root common ancestor' do
123+
keys = %w[A/A/log.txt A/B/log.txt A/C/log.txt]
124+
expect_any_instance_of(Aws::S3::AccessGrantsCredentialsProvider)
125+
.to receive(:access_grants_credentials_for)
126+
.with(bucket: 'bucket', key: 'A/', permission: 'WRITE', prefix: nil)
127+
delete = { objects: keys.map { |key| { key: key } } }
128+
client.delete_objects(bucket: 'bucket', delete: delete)
129+
end
130+
131+
it 'key with level next to root common ancestor' do
132+
keys = %w[A/path12/log.txt A/path34/B/log.txt A/path56/C/log.txt]
133+
expect_any_instance_of(Aws::S3::AccessGrantsCredentialsProvider)
134+
.to receive(:access_grants_credentials_for)
135+
.with(bucket: 'bucket', key: 'A/path', permission: 'WRITE', prefix: nil)
136+
delete = { objects: keys.map { |key| { key: key } } }
137+
client.delete_objects(bucket: 'bucket', delete: delete)
138+
end
139+
end
140+
141+
context 'copy_source' do
142+
it 'key with no common ancestor' do
143+
expect_any_instance_of(Aws::S3::AccessGrantsCredentialsProvider)
144+
.to receive(:access_grants_credentials_for)
145+
.with(bucket: 'bucket', key: '', permission: 'READWRITE', prefix: nil)
146+
client.copy_object(bucket: 'bucket', key: 'A/log.txt', copy_source: 'bucket/B/log.txt')
147+
end
148+
149+
it 'key with root common ancestor' do
150+
expect_any_instance_of(Aws::S3::AccessGrantsCredentialsProvider)
151+
.to receive(:access_grants_credentials_for)
152+
.with(bucket: 'bucket', key: 'A/', permission: 'READWRITE', prefix: nil)
153+
client.copy_object(bucket: 'bucket', key: 'A/A/log.txt', copy_source: 'bucket/A/B/log.txt')
154+
end
155+
156+
it 'key with level next to root common ancestor' do
157+
expect_any_instance_of(Aws::S3::AccessGrantsCredentialsProvider)
158+
.to receive(:access_grants_credentials_for)
159+
.with(bucket: 'bucket', key: 'A/path', permission: 'READWRITE', prefix: nil)
160+
client.copy_object(bucket: 'bucket', key: 'A/path12/log.txt', copy_source: 'bucket/A/path34/log.txt')
161+
end
162+
end
111163
end
112164
end
113165
end

0 commit comments

Comments
 (0)