1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
|
# frozen_string_literal: true
module Aws
module S3
# @api private
def self.access_grants_credentials_cache
@access_grants_credentials_cache ||= LRUCache.new(max_entries: 100)
end
# @api private
def self.access_grants_account_id_cache
@access_grants_account_id_cache ||= LRUCache.new(
max_entries: 100,
expiration: 60 * 10
)
end
# Returns Credentials class for S3 Access Grants. Accepts GetDataAccess
# params and other configuration as options. See
# {Aws::S3Control::Client#get_data_access} for details.
class AccessGrantsCredentialsProvider
# @param [Hash] options
# @option options [Hash] :s3_control_client_options The S3 Control
# client options used to create regional S3 Control clients to
# create the session. Region will be set to the region of the
# bucket.
# @option options [Aws::STS::Client] :sts_client The STS client used for
# fetching the Account ID for the credentials if credentials do not
# include an Account ID.
# @option options [Aws::S3::Client] :s3_client The S3 client used for
# fetching the location of the bucket so that a regional S3 Control
# client can be created. Defaults to the S3 client from the access
# grants plugin.
# @option options [String] :privilege ('Default') The privilege to use
# when requesting credentials. (see: {Aws::S3Control::Client#get_data_access})
# @option options [Boolean] :fallback (false) When true, if access is
# denied, the provider will fall back to the configured credentials.
# @option options [Boolean] :caching (true) When true, credentials and
# bucket account ids will be cached.
# @option options [Callable] :before_refresh Proc called before
# credentials are refreshed.
def initialize(options = {})
@s3_control_options = options.delete(:s3_control_client_options) || {}
@s3_client = options.delete(:s3_client)
@sts_client = options.delete(:sts_client)
@fallback = options.delete(:fallback) || false
@caching = options.delete(:caching) != false
@s3_control_clients = {}
@bucket_region_cache = Aws::S3.bucket_region_cache
@head_bucket_mutex = Mutex.new
@head_bucket_call = false
return unless @caching
@credentials_cache = Aws::S3.access_grants_credentials_cache
@account_id_cache = Aws::S3.access_grants_account_id_cache
end
def access_grants_credentials_for(options = {})
target = target_prefix(
options[:bucket],
options[:key],
options[:prefix]
)
credentials = s3_client.config.credentials.credentials # resolves
if @caching
cached_credentials_for(target, options[:permission], credentials)
else
new_credentials_for(target, options[:permission], credentials)
end
rescue Aws::S3Control::Errors::AccessDenied
raise unless @fallback
warn 'Access denied for S3 Access Grants. Falling back to ' \
'configured credentials.'
s3_client.config.credentials
end
attr_accessor :s3_client
private
def s3_control_client(bucket_region)
@s3_control_clients[bucket_region] ||= begin
credentials = s3_client.config.credentials
config = { credentials: credentials }.merge(@s3_control_options)
Aws::S3Control::Client.new(config.merge(
region: bucket_region,
use_fips_endpoint: s3_client.config.use_fips_endpoint,
use_dualstack_endpoint: s3_client.config.use_dualstack_endpoint
))
end
end
def cached_credentials_for(target, permission, credentials)
cached_creds = broad_search_credentials_cache_prefix(target, permission, credentials)
return cached_creds if cached_creds
if %w[READ WRITE].include?(permission)
cached_creds = broad_search_credentials_cache_prefix(target, 'READWRITE', credentials)
return cached_creds if cached_creds
end
cached_creds = broad_search_credentials_cache_characters(target, permission, credentials)
return cached_creds if cached_creds
if %w[READ WRITE].include?(permission)
cached_creds = broad_search_credentials_cache_characters(target, 'READWRITE', credentials)
return cached_creds if cached_creds
end
creds = new_credentials_for(target, permission, credentials)
if creds.matched_grant_target.end_with?('*')
# remove /* from the end of the target
key = credentials_cache_key(creds.matched_grant_target[0...-2], permission, credentials)
@credentials_cache[key] = creds
end
creds
end
def broad_search_credentials_cache_prefix(target, permission, credentials)
prefix = target
while prefix != 's3:'
key = credentials_cache_key(prefix, permission, credentials)
return @credentials_cache[key] if @credentials_cache.key?(key)
prefix = prefix.split('/', -1)[0..-2].join('/')
end
nil
end
def broad_search_credentials_cache_characters(target, permission, credentials)
prefix = target
while prefix != 's3://'
key = credentials_cache_key("#{prefix}*", permission, credentials)
return @credentials_cache[key] if @credentials_cache.key?(key)
prefix = prefix[0..-2]
end
nil
end
def new_credentials_for(target, permission, credentials)
bucket_region = bucket_region_for_access_grants(target)
client = s3_control_client(bucket_region)
AccessGrantsCredentials.new(
target: target,
account_id: account_id_for_access_grants(target, credentials),
permission: permission,
client: client
)
end
def account_id_for_access_grants(target, credentials)
if @caching
cached_account_id_for(target, credentials)
else
new_account_id_for(target, credentials)
end
end
def cached_account_id_for(target, credentials)
bucket = bucket_name_from(target)
if @account_id_cache.key?(bucket)
@account_id_cache[bucket]
else
@account_id_cache[bucket] = new_account_id_for(target, credentials)
end
end
# returns the account id associated with the access grants instance
def new_account_id_for(target, credentials)
bucket_region = bucket_region_for_access_grants(target)
s3_control_client = s3_control_client(bucket_region)
resp = s3_control_client.get_access_grants_instance_for_prefix(
s3_prefix: target,
account_id: account_id_for_credentials(bucket_region, credentials)
)
ARNParser.parse(resp.access_grants_instance_arn).account_id
end
def bucket_region_for_access_grants(target)
bucket = bucket_name_from(target)
# regardless of caching option, bucket region cache is always shared
cached_bucket_region_for(bucket)
end
def cached_bucket_region_for(bucket)
if @bucket_region_cache.key?(bucket)
@bucket_region_cache[bucket]
else
@bucket_region_cache[bucket] = new_bucket_region_for(bucket)
end
end
def new_bucket_region_for(bucket)
@head_bucket_mutex.synchronize do
begin
@head_bucket_call = true
@s3_client.head_bucket(bucket: bucket).bucket_region
rescue Aws::S3::Errors::Http301Error => e
e.data.region
ensure
@head_bucket_call = false
end
end
end
# returns the account id for the configured credentials
def account_id_for_credentials(region, credentials)
# use resolved credentials to check for account id
if credentials.respond_to?(:account_id) && credentials.account_id &&
!credentials.account_id.empty?
credentials.account_id
else
@sts_client ||= Aws::STS::Client.new(
credentials: s3_client.config.credentials,
region: region,
use_fips_endpoint: s3_client.config.use_fips_endpoint,
use_dualstack_endpoint: s3_client.config.use_dualstack_endpoint
)
@sts_client.get_caller_identity.account
end
end
def target_prefix(bucket, key, prefix)
if key && !key.empty?
"s3://#{bucket}/#{key}"
elsif prefix && !prefix.empty?
"s3://#{bucket}/#{prefix}"
else
"s3://#{bucket}/*"
end
end
def credentials_cache_key(target, permission, credentials)
"#{credentials.access_key_id}-#{credentials.secret_access_key}" \
"-#{permission}-#{target}"
end
# extracts bucket name from target prefix
def bucket_name_from(target)
URI(target).host
end
end
end
end
|