lib/fog/storage/google_json/real.rb
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