File: credential_fetcher.rb

package info (click to toggle)
ruby-fog-aws 3.18.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 8,140 kB
  • sloc: ruby: 73,328; javascript: 14; makefile: 9; sh: 4
file content (155 lines) | stat: -rw-r--r-- 6,831 bytes parent folder | download | duplicates (2)
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
# frozen_string_literal: true

require 'securerandom'

module Fog
  module AWS
    module CredentialFetcher

      INSTANCE_METADATA_HOST = "http://169.254.169.254"
      INSTANCE_METADATA_TOKEN = "/latest/api/token"
      INSTANCE_METADATA_PATH = "/latest/meta-data/iam/security-credentials/"
      INSTANCE_METADATA_AZ = "/latest/meta-data/placement/availability-zone/"

      CONTAINER_CREDENTIALS_HOST = "http://169.254.170.2"

      module ServiceMethods
        def fetch_credentials(options)
          if options[:use_iam_profile] && Fog.mocking?
            return Fog::AWS::Compute::Mock.data[:iam_role_based_creds]
          end
          if options[:use_iam_profile]
            begin
              role_data = nil
              region = options[:region] || ENV["AWS_DEFAULT_REGION"]

              if ENV["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"]
                connection = options[:connection] || Excon.new(CONTAINER_CREDENTIALS_HOST)
                credential_path = options[:credential_path] || ENV["AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"]
                role_data = connection.get(:path => credential_path, :idempotent => true, :expects => 200).body
                session = Fog::JSON.decode(role_data)

                if region.nil?
                  connection = options[:metadata_connection] || Excon.new(INSTANCE_METADATA_HOST)
                  token_header = fetch_credentials_token_header(connection, options[:disable_imds_v2])
                  region = connection.get(:path => INSTANCE_METADATA_AZ, :idempotent => true, :expects => 200, :headers => token_header).body[0..-2]
                end
              elsif ENV["AWS_WEB_IDENTITY_TOKEN_FILE"]
                params = {
                  :Action => "AssumeRoleWithWebIdentity",
                  :RoleArn => options[:role_arn] || ENV.fetch("AWS_ROLE_ARN"),
                  :RoleSessionName => options[:role_session_name] || ENV["AWS_ROLE_SESSION_NAME"] || "fog-aws-#{SecureRandom.hex}",
                  :WebIdentityToken => File.read(options[:aws_web_identity_token_file] || ENV.fetch("AWS_WEB_IDENTITY_TOKEN_FILE")),
                  :Version => "2011-06-15",
                }

                sts_endpoint =
                  if ENV["AWS_STS_REGIONAL_ENDPOINTS"] == "regional" && region
                    "https://sts.#{region}.amazonaws.com"
                  else
                    "https://sts.amazonaws.com"
                  end

                connection = options[:connection] || Excon.new(sts_endpoint, :query => params)
                document = Nokogiri::XML(connection.get(:idempotent => true, :expects => 200).body)

                session = {
                  "AccessKeyId" => document.css("AccessKeyId").children.text,
                  "SecretAccessKey" => document.css("SecretAccessKey").children.text,
                  "Token" => document.css("SessionToken").children.text,
                  "Expiration" => document.css("Expiration").children.text,
                }

                if region.nil?
                  connection = options[:metadata_connection] || Excon.new(INSTANCE_METADATA_HOST)
                  token_header = fetch_credentials_token_header(connection, options[:disable_imds_v2])
                  region = connection.get(:path => INSTANCE_METADATA_AZ, :idempotent => true, :expects => 200, :headers => token_header).body[0..-2]
                end
              else
                connection = options[:connection] || Excon.new(INSTANCE_METADATA_HOST)
                token_header = fetch_credentials_token_header(connection, options[:disable_imds_v2])
                role_name = connection.get(:path => INSTANCE_METADATA_PATH, :idempotent => true, :expects => 200, :headers => token_header).body
                role_data = connection.get(:path => INSTANCE_METADATA_PATH+role_name, :idempotent => true, :expects => 200, :headers => token_header).body
                session = Fog::JSON.decode(role_data)

                region ||= connection.get(:path => INSTANCE_METADATA_AZ, :idempotent => true, :expects => 200, :headers => token_header).body[0..-2]
              end

              credentials = {}
              credentials[:aws_access_key_id] = session['AccessKeyId']
              credentials[:aws_secret_access_key] = session['SecretAccessKey']
              credentials[:aws_session_token] = session['Token']
              credentials[:aws_credentials_expire_at] = Time.xmlschema session['Expiration']

              # set region by default to the one the instance is in.
              credentials[:region] = region
              credentials[:sts_endpoint] = sts_endpoint if sts_endpoint
              #these indicate the metadata service is unavailable or has no profile setup
              credentials
            rescue Excon::Error => e
              Fog::Logger.warning("Unable to fetch credentials: #{e.message}")
              super
            end
          else
            super
          end
        end

        def fetch_credentials_token_header(connection, disable_imds_v2)
          return nil if disable_imds_v2

          token = connection.put(
            :path => INSTANCE_METADATA_TOKEN,
            :idempotent => true,
            :expects => 200,
            :retry_interval => 1,
            :retry_limit => 3,
            :read_timeout => 1,
            :write_timeout => 1,
            :connect_timeout => 1,
            :headers => { "X-aws-ec2-metadata-token-ttl-seconds" => "300" }
          ).body

          { "X-aws-ec2-metadata-token" => token }
        rescue Excon::Error
          nil
        end
      end

      module ConnectionMethods
        def refresh_credentials_if_expired
          refresh_credentials if credentials_expired?
        end

        private

        # When defined, 'aws_credentials_refresh_threshold_seconds' controls
        # when the credential needs to be refreshed, expressed in seconds before
        # the current credential's expiration time
        def credentials_refresh_threshold
          @aws_credentials_refresh_threshold_seconds || 15
        end

        def credentials_expired?
          @use_iam_profile &&
            (!@aws_credentials_expire_at ||
             (@aws_credentials_expire_at && Fog::Time.now > @aws_credentials_expire_at - credentials_refresh_threshold)) #new credentials become available from around 5 minutes before expiration time
        end

        def refresh_credentials
          if @use_iam_profile
            new_credentials = service.fetch_credentials :use_iam_profile => @use_iam_profile, :region => @region
            if new_credentials.any?
              setup_credentials new_credentials
              return true
            else
              false
            end
          else
            false
          end
        end
      end
    end
  end
end