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
|