fog/fog-google

View on GitHub
lib/fog/storage/google_json/real.rb

Summary

Maintainability
A
2 hrs
Test Coverage
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 = options.dup
          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)

          @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 = @storage_json.authorization.signing_key
          key = OpenSSL::PKey::RSA.new(@storage_json.authorization.signing_key) unless key.respond_to?(:sign)
          digest = OpenSSL::Digest::SHA256.new
          return key.sign(digest, string_to_sign)
        end

        # IAM client used for SignBlob API.
        # Lazily initialize this since it requires another authorization request.
        def iam_service
          return @iam_service if defined?(@iam_service)

          @iam_service = ::Google::Apis::IamcredentialsV1::IAMCredentialsService.new
          apply_client_options(@iam_service, @options)
          iam_options = @options.merge(google_api_scope_url: GOOGLE_STORAGE_JSON_IAM_API_SCOPE_URLS.join(" "))
          @iam_service.authorization = initialize_auth(iam_options)
          @iam_service
        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