File: real.rb

package info (click to toggle)
ruby-fog-google 1.19.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,568 kB
  • sloc: ruby: 16,775; makefile: 3
file content (171 lines) | stat: -rw-r--r-- 6,999 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
module Fog
  module Storage
    class GoogleJSON
      class Real
        include Utils
        include Fog::Google::Shared

        attr_accessor :client
        attr_reader :storage_json

        def initialize(options = {})
          shared_initialize(options[:google_project], GOOGLE_STORAGE_JSON_API_VERSION, GOOGLE_STORAGE_JSON_BASE_URL)
          options[:google_api_scope_url] = GOOGLE_STORAGE_JSON_API_SCOPE_URLS.join(" ")
          @host = options[:host] || "storage.googleapis.com"

          # TODO(temikus): Do we even need this client?
          @client = initialize_google_client(options)
          # IAM client used for SignBlob API
          @iam_service = ::Google::Apis::IamcredentialsV1::IAMCredentialsService.new
          apply_client_options(@iam_service, {
                                 google_api_scope_url: GOOGLE_STORAGE_JSON_IAM_API_SCOPE_URLS.join(" ")
                               })

          @storage_json = ::Google::Apis::StorageV1::StorageService.new
          apply_client_options(@storage_json, options)

          @storage_json.client_options.open_timeout_sec = options[:open_timeout_sec] if options[:open_timeout_sec]
          @storage_json.client_options.read_timeout_sec = options[:read_timeout_sec] if options[:read_timeout_sec]
          @storage_json.client_options.send_timeout_sec = options[:send_timeout_sec] if options[:send_timeout_sec]
        end

        def signature(params)
          string_to_sign = <<-DATA
#{params[:method]}
#{params[:headers]['Content-MD5']}
#{params[:headers]['Content-Type']}
#{params[:headers]['Date']}
DATA

          google_headers = {}
          canonical_google_headers = ""
          params[:headers].each do |key, value|
            google_headers[key] = value if key[0..6] == "x-goog-"
          end

          google_headers = google_headers.sort_by { |a| a[0] }
          google_headers.each do |key, value|
            canonical_google_headers << "#{key}:#{value}\n"
          end
          string_to_sign << canonical_google_headers.to_s

          canonical_resource = "/"
          if subdomain = params.delete(:subdomain)
            canonical_resource << "#{CGI.escape(subdomain).downcase}/"
          end
          canonical_resource << params[:path].to_s
          canonical_resource << "?"
          (params[:query] || {}).each_key do |key|
            if %w(acl cors location logging requestPayment versions versioning).include?(key)
              canonical_resource << "#{key}&"
            end
          end
          canonical_resource.chop!
          string_to_sign << canonical_resource.to_s

          # TODO(temikus): make signer configurable or add ability to supply your own via lambda
          if !@storage_json.authorization.signing_key.nil?
            signed_string = default_signer(string_to_sign)
          else
            # If client doesn't contain signing key attempt to auth via IAM SignBlob API
            signed_string = iam_signer(string_to_sign)
          end

          Base64.encode64(signed_string).chomp!
        end

        private

        def google_access_id
          @google_access_id ||= get_google_access_id
        end

        ##
        # Fetches the google service account name
        #
        # @return [String] Service account name, typically needed for GoogleAccessId, e.g.
        #   my-account@project.iam.gserviceaccount
        # @raises [Fog::Errors::Error] If authorisation is incorrect or inapplicable for current action
        def get_google_access_id
          if @storage_json.authorization.is_a?(::Google::Auth::UserRefreshCredentials)
            raise Fog::Errors::Error.new("User / Application Default Credentials are not supported for storage"\
                                         "url signing, please use a service account or metadata authentication.")
          end

          if !@storage_json.authorization.issuer.nil?
            return @storage_json.authorization.issuer
          else
            get_access_id_from_metadata
          end
        end

        ##
        # Attempts to fetch the google service account name from metadata using Google::Cloud::Env
        #
        # @return [String] Service account name, typically needed for GoogleAccessId, e.g.
        #   my-account@project.iam.gserviceaccount
        # @raises [Fog::Errors::Error] If Metadata service is not available or returns an invalid response
        def get_access_id_from_metadata
          if @google_cloud_env.metadata?
            access_id = @google_cloud_env.lookup_metadata("instance", "service-accounts/default/email")
          else
            raise Fog::Errors::Error.new("Metadata service not available, unable to retrieve service account info.")
          end

          if access_id.nil?
            raise Fog::Errors::Error.new("Metadata service found but didn't return data." \
               "Please file a bug: https://github.com/fog/fog-google")
          end

          return access_id
        end

        ##
        # Default url signer using service account keys
        #
        # @param [String] string_to_sign Special collection of headers and options for V2 storage signing, e.g.:
        #
        #   StringToSign = HTTP_Verb + "\n" +
        #                  Content_MD5 + "\n" +
        #                  Content_Type + "\n" +
        #                  Expires + "\n" +
        #                  Canonicalized_Extension_Headers +
        #                  Canonicalized_Resource
        #
        #   See https://cloud.google.com/storage/docs/access-control/signed-urls-v2
        # @return [String] Signature binary blob
        def default_signer(string_to_sign)
          key = OpenSSL::PKey::RSA.new(@storage_json.authorization.signing_key)
          digest = OpenSSL::Digest::SHA256.new
          return key.sign(digest, string_to_sign)
        end

        ##
        # Fallback URL signer using the IAM SignServiceAccountBlob API, see
        #   Google::Apis::IamcredentialsV1::IAMCredentialsService#sign_service_account_blob
        #
        # @param [String] string_to_sign Special collection of headers and options for V2 storage signing, e.g.:
        #
        #   StringToSign = HTTP_Verb + "\n" +
        #                  Content_MD5 + "\n" +
        #                  Content_Type + "\n" +
        #                  Expires + "\n" +
        #                  Canonicalized_Extension_Headers +
        #                  Canonicalized_Resource
        #
        #   See https://cloud.google.com/storage/docs/access-control/signed-urls-v2
        # @return [String] Signature binary blob
        def iam_signer(string_to_sign)
          request = ::Google::Apis::IamcredentialsV1::SignBlobRequest.new(
            payload: string_to_sign
          )

          resource = "projects/-/serviceAccounts/#{google_access_id}"
          response = @iam_service.sign_service_account_blob(resource, request)

          return response.signed_blob
        end
      end
    end
  end
end