File: credentials_loader.rb

package info (click to toggle)
ruby-googleauth 1.16.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 492 kB
  • sloc: ruby: 3,194; makefile: 4
file content (198 lines) | stat: -rw-r--r-- 8,958 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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# Copyright 2015 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require "os"
require "rbconfig"

require "googleauth/errors"

module Google
  # Module Auth provides classes that provide Google-specific authorization
  # used to access Google APIs.
  module Auth
    # CredentialsLoader contains the behaviour used to locate and find default
    # credentials files on the file system.
    module CredentialsLoader
      ENV_VAR                   = "GOOGLE_APPLICATION_CREDENTIALS".freeze
      PRIVATE_KEY_VAR           = "GOOGLE_PRIVATE_KEY".freeze
      CLIENT_EMAIL_VAR          = "GOOGLE_CLIENT_EMAIL".freeze
      CLIENT_ID_VAR             = "GOOGLE_CLIENT_ID".freeze
      CLIENT_SECRET_VAR         = "GOOGLE_CLIENT_SECRET".freeze
      REFRESH_TOKEN_VAR         = "GOOGLE_REFRESH_TOKEN".freeze
      ACCOUNT_TYPE_VAR          = "GOOGLE_ACCOUNT_TYPE".freeze
      PROJECT_ID_VAR            = "GOOGLE_PROJECT_ID".freeze
      AWS_REGION_VAR            = "AWS_REGION".freeze
      AWS_DEFAULT_REGION_VAR    = "AWS_DEFAULT_REGION".freeze
      AWS_ACCESS_KEY_ID_VAR     = "AWS_ACCESS_KEY_ID".freeze
      AWS_SECRET_ACCESS_KEY_VAR = "AWS_SECRET_ACCESS_KEY".freeze
      AWS_SESSION_TOKEN_VAR     = "AWS_SESSION_TOKEN".freeze
      GCLOUD_POSIX_COMMAND      = "gcloud".freeze
      GCLOUD_WINDOWS_COMMAND    = "gcloud.cmd".freeze
      GCLOUD_CONFIG_COMMAND     = "config config-helper --format json --verbosity none --quiet".freeze

      CREDENTIALS_FILE_NAME = "application_default_credentials.json".freeze
      NOT_FOUND_ERROR = "Unable to read the credential file specified by #{ENV_VAR}".freeze
      WELL_KNOWN_PATH = "gcloud/#{CREDENTIALS_FILE_NAME}".freeze
      WELL_KNOWN_ERROR = "Unable to read the default credential file".freeze

      SYSTEM_DEFAULT_ERROR = "Unable to read the system default credential file".freeze

      CLOUD_SDK_CLIENT_ID = "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app" \
                            "s.googleusercontent.com".freeze

      # make_creds proxies the construction of a credentials instance
      #
      # By default, it calls #new on the current class, but this behaviour can
      # be modified, allowing different instances to be created.
      def make_creds *args
        creds = new(*args)
        creds = creds.configure_connection args[0] if creds.respond_to?(:configure_connection) && args.size == 1
        creds
      end

      # Creates an instance from the path specified in an environment
      # variable.
      #
      # @param scope [string|array|nil] the scope(s) to access
      # @param options [Hash] Connection options. These may be used to configure
      #     how OAuth tokens are retrieved, by providing a suitable
      #     `Faraday::Connection`. For example, if a connection proxy must be
      #     used in the current network, you may provide a connection with
      #     with the needed proxy options.
      #     The following keys are recognized:
      #     * `:default_connection` The connection object to use.
      #     * `:connection_builder` A `Proc` that returns a connection.
      # @raise [Google::Auth::InitializationError] If the credentials file cannot be read
      def from_env scope = nil, options = {}
        options = interpret_options scope, options
        if ENV.key?(ENV_VAR) && !ENV[ENV_VAR].empty?
          path = ENV[ENV_VAR]
          raise InitializationError, "file #{path} does not exist" unless File.exist? path
          File.open path do |f|
            return make_creds options.merge(json_key_io: f)
          end
        elsif service_account_env_vars? || authorized_user_env_vars?
          make_creds options
        end
      rescue StandardError => e
        raise InitializationError, "#{NOT_FOUND_ERROR}: #{e}"
      end

      # Creates an instance from a well known path.
      #
      # @param scope [string|array|nil] the scope(s) to access
      # @param options [Hash] Connection options. These may be used to configure
      #     how OAuth tokens are retrieved, by providing a suitable
      #     `Faraday::Connection`. For example, if a connection proxy must be
      #     used in the current network, you may provide a connection with
      #     with the needed proxy options.
      #     The following keys are recognized:
      #     * `:default_connection` The connection object to use.
      #     * `:connection_builder` A `Proc` that returns a connection.
      # @raise [Google::Auth::InitializationError] If the credentials file cannot be read
      def from_well_known_path scope = nil, options = {}
        options = interpret_options scope, options
        home_var = OS.windows? ? "APPDATA" : "HOME"
        base = WELL_KNOWN_PATH
        root = ENV[home_var].nil? ? "" : ENV[home_var]
        base = File.join ".config", base unless OS.windows?
        path = File.join root, base
        return nil unless File.exist? path
        File.open path do |f|
          return make_creds options.merge(json_key_io: f)
        end
      rescue StandardError => e
        raise InitializationError, "#{WELL_KNOWN_ERROR}: #{e}"
      end

      # Creates an instance from the system default path
      #
      # @param scope [string|array|nil] the scope(s) to access
      # @param options [Hash] Connection options. These may be used to configure
      #     how OAuth tokens are retrieved, by providing a suitable
      #     `Faraday::Connection`. For example, if a connection proxy must be
      #     used in the current network, you may provide a connection with
      #     with the needed proxy options.
      #     The following keys are recognized:
      #     * `:default_connection` The connection object to use.
      #     * `:connection_builder` A `Proc` that returns a connection.
      # @raise [Google::Auth::InitializationError] If the credentials file cannot be read or is invalid
      def from_system_default_path scope = nil, options = {}
        options = interpret_options scope, options
        if OS.windows?
          return nil unless ENV["ProgramData"]
          prefix = File.join ENV["ProgramData"], "Google/Auth"
        else
          prefix = "/etc/google/auth/"
        end
        path = File.join prefix, CREDENTIALS_FILE_NAME
        return nil unless File.exist? path
        File.open path do |f|
          return make_creds options.merge(json_key_io: f)
        end
      rescue StandardError => e
        raise InitializationError, "#{SYSTEM_DEFAULT_ERROR}: #{e}"
      end

      module_function

      # Finds project_id from gcloud CLI configuration
      def load_gcloud_project_id
        gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows?
        gcloud = GCLOUD_POSIX_COMMAND unless OS.windows?
        gcloud_json = IO.popen("#{gcloud} #{GCLOUD_CONFIG_COMMAND}", err: :close, &:read)
        config = MultiJson.load gcloud_json
        config["configuration"]["properties"]["core"]["project"]
      rescue StandardError
        nil
      end

      # @private
      # Loads a JSON key from an IO object, verifies its type, and rewinds the IO.
      #
      # @param json_key_io [IO] An IO object containing the JSON key.
      # @param expected_type [String] The expected credential type name.
      # @raise [Google::Auth::InitializationError] If the JSON key type does not match the expected type.
      def load_and_verify_json_key_type json_key_io, expected_type
        json_key = MultiJson.load json_key_io.read
        json_key_io.rewind # Rewind the stream so it can be read again.
        return if json_key["type"] == expected_type
        raise Google::Auth::InitializationError,
              "The provided credentials were not of type '#{expected_type}'. " \
              "Instead, the type was '#{json_key['type']}'."
      end

      private

      def interpret_options scope, options
        if scope.is_a? Hash
          options = scope
          scope = nil
        end
        return options.merge scope: scope if scope && !options[:scope]
        options
      end

      def service_account_env_vars?
        ([PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR] - ENV.keys).empty? &&
          !ENV.to_h.fetch_values(PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR).join(" ").empty?
      end

      def authorized_user_env_vars?
        ([CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR] - ENV.keys).empty? &&
          !ENV.to_h.fetch_values(CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR).join(" ").empty?
      end
    end
  end
end