File: access_grants_credentials_provider.rb

package info (click to toggle)
ruby-aws-sdk-s3 1.170.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,740 kB
  • sloc: ruby: 16,388; makefile: 3
file content (250 lines) | stat: -rw-r--r-- 9,099 bytes parent folder | download
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