Skip to content

Commit

Permalink
Enables reading credentials from env vars.
Browse files Browse the repository at this point in the history
- ServiceAccountCredentials, ServiceAccountJwtHeaderCredentials
and UserRefreshCredentials initializers now take keyword args
via options hash.

- In `credentials_loader.rb`, refactored env var checking into
private methods

- Updated tests & added new tests.

- Fixed existing test for #from_well_known_path 'fails if
the file is invalid', where `from_env` was called instead of
`from_well_known_path`.

- Fixed rubocop errors I introduced, but two existing ones remain.

- Added entry to changelog.

- Fixed rubocop errors from code containing parallel assignments

- Updated rubocop_todo.yml to ignore parallel assignments and
trailing underscore assignments.
  • Loading branch information
Herbert Siojo committed May 21, 2015
1 parent 133e05c commit 490278e
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 71 deletions.
14 changes: 12 additions & 2 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This configuration was generated by `rubocop --auto-gen-config`
# on 2015-04-23 11:18:24 -0700 using RuboCop version 0.30.0.
# on 2015-05-18 09:38:28 -0700 using RuboCop version 0.31.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand All @@ -9,7 +9,17 @@
Metrics/AbcSize:
Max: 24

# Offense count: 6
# Offense count: 10
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 13

# Offense count: 1
# Cop supports --auto-correct.
Performance/ParallelAssignment:
Enabled: false

# Offense count: 1
# Cop supports --auto-correct.
Style/TrailingUnderscoreVariable:
Enabled: false
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
### Changes

* Enables passing credentials via environment variables. ([@haabaato][])
[#27](https://github.com/google/google-auth-library-ruby/issues/27)

## 0.4.1 (25/04/2015)

### Changes
Expand All @@ -20,3 +25,4 @@

[@tbetbetbe]: https://github.com/tbetbetbe
[@joneslee85]: https://github.com/joneslee85
[@haabaato]: https://github.com/haabaato
32 changes: 27 additions & 5 deletions lib/googleauth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,38 @@ class DefaultCredentials

# override CredentialsLoader#make_creds to use the class determined by
# loading the json.
def self.make_creds(json_key_io, scope = nil)
json_key, clz = determine_creds_class(json_key_io)
clz.new(StringIO.new(MultiJson.dump(json_key)), scope)
def self.make_creds(options = {})
json_key_io, scope = options.values_at(:json_key_io, :scope)
if json_key_io
json_key, clz = determine_creds_class(json_key_io)
clz.new(json_key_io: StringIO.new(MultiJson.dump(json_key)),
scope: scope)
else
clz = read_creds
clz.new(scope: scope)
end
end

def self.read_creds
env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
type = ENV[env_var]
fail "#{ACCOUNT_TYPE_VAR} is undefined in env" unless type
case type
when 'service_account'
ServiceAccountCredentials
when 'authorized_user'
UserRefreshCredentials
else
fail "credentials type '#{type}' is not supported"
end
end

# Reads the input json and determines which creds class to use.
def self.determine_creds_class(json_key_io)
json_key = MultiJson.load(json_key_io.read)
fail "the json is missing the #{key} field" unless json_key.key?('type')
type = json_key['type']
key = 'type'
fail "the json is missing the '#{key}' field" unless json_key.key?(key)
type = json_key[key]
case type
when 'service_account'
[json_key, ServiceAccountCredentials]
Expand Down
34 changes: 28 additions & 6 deletions lib/googleauth/credentials_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ module Auth
module CredentialsLoader
extend Memoist
ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'

PRIVATE_KEY_VAR = 'GOOGLE_PRIVATE_KEY'
CLIENT_EMAIL_VAR = 'GOOGLE_CLIENT_EMAIL'
CLIENT_ID_VAR = 'GOOGLE_CLIENT_ID'
CLIENT_SECRET_VAR = 'GOOGLE_CLIENT_SECRET'
REFRESH_TOKEN_VAR = 'GOOGLE_REFRESH_TOKEN'
ACCOUNT_TYPE_VAR = 'GOOGLE_ACCOUNT_TYPE'

NOT_FOUND_ERROR =
"Unable to read the credential file specified by #{ENV_VAR}"
WELL_KNOWN_PATH = 'gcloud/application_default_credentials.json'
Expand All @@ -63,11 +71,14 @@ def make_creds(*args)
#
# @param scope [string|array|nil] the scope(s) to access
def from_env(scope = nil)
return nil unless ENV.key?(ENV_VAR)
path = ENV[ENV_VAR]
fail 'file #{path} does not exist' unless File.exist?(path)
File.open(path) do |f|
return make_creds(f, scope)
if ENV.key?(ENV_VAR)
path = ENV[ENV_VAR]
fail "file #{path} does not exist" unless File.exist?(path)
File.open(path) do |f|
return make_creds(json_key_io: f, scope: scope)
end
elsif service_account_env_vars? || authorized_user_env_vars?
return make_creds(scope: scope)
end
rescue StandardError => e
raise "#{NOT_FOUND_ERROR}: #{e}"
Expand All @@ -83,11 +94,22 @@ def from_well_known_path(scope = nil)
path = File.join(root, base)
return nil unless File.exist?(path)
File.open(path) do |f|
return make_creds(f, scope)
return make_creds(json_key_io: f, scope: scope)
end
rescue StandardError => e
raise "#{WELL_KNOWN_ERROR}: #{e}"
end

private

def service_account_env_vars?
([PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR] - ENV.keys).empty?
end

def authorized_user_env_vars?
([CLIENT_ID_VAR, CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR] -
ENV.keys).empty?
end
end
end
end
25 changes: 19 additions & 6 deletions lib/googleauth/service_account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,15 @@ def self.read_json_key(json_key_io)
#
# @param json_key_io [IO] an IO from which the JSON key can be read
# @param scope [string|array|nil] the scope(s) to access
def initialize(json_key_io, scope = nil)
private_key, client_email = self.class.read_json_key(json_key_io)
def initialize(options = {})
json_key_io, scope = options.values_at(:json_key_io, :scope)
if json_key_io
private_key, client_email = self.class.read_json_key(json_key_io)
else
private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
end

super(token_credential_uri: TOKEN_CRED_URI,
audience: TOKEN_CRED_URI,
scope: scope,
Expand All @@ -90,7 +97,7 @@ def apply!(a_hash, opts = {})
client_email: @issuer
}
alt_clz = ServiceAccountJwtHeaderCredentials
alt = alt_clz.new(StringIO.new(MultiJson.dump(cred_json)))
alt = alt_clz.new(json_key_io: StringIO.new(MultiJson.dump(cred_json)))
alt.apply!(a_hash)
end
end
Expand Down Expand Up @@ -120,7 +127,7 @@ class ServiceAccountJwtHeaderCredentials
# optional scope. Here's the constructor only has one param, so
# we modify make_creds to reflect this.
def self.make_creds(*args)
new(args[0])
new(json_key_io: args[0][:json_key_io])
end

# Reads the private key and client email fields from the service account
Expand All @@ -135,8 +142,14 @@ def self.read_json_key(json_key_io)
# Initializes a ServiceAccountJwtHeaderCredentials.
#
# @param json_key_io [IO] an IO from which the JSON key can be read
def initialize(json_key_io)
private_key, client_email = self.class.read_json_key(json_key_io)
def initialize(options = {})
json_key_io = options[:json_key_io]
if json_key_io
private_key, client_email = self.class.read_json_key(json_key_io)
else
private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR]
client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR]
end
@private_key = private_key
@issuer = client_email
@signing_key = OpenSSL::PKey::RSA.new(private_key)
Expand Down
11 changes: 9 additions & 2 deletions lib/googleauth/user_refresh.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,15 @@ def self.read_json_key(json_key_io)
#
# @param json_key_io [IO] an IO from which the JSON key can be read
# @param scope [string|array|nil] the scope(s) to access
def initialize(json_key_io, scope = nil)
user_creds = self.class.read_json_key(json_key_io)
def initialize(options = {})
json_key_io, scope = options.values_at(:json_key_io, :scope)
user_creds = self.class.read_json_key(json_key_io) if json_key_io
user_creds ||= {
client_id: ENV[CredentialsLoader::CLIENT_ID_VAR],
client_secret: ENV[CredentialsLoader::CLIENT_SECRET_VAR],
refresh_token: ENV[CredentialsLoader::REFRESH_TOKEN_VAR]
}

super(token_credential_uri: TOKEN_CRED_URI,
client_id: user_creds['client_id'],
client_secret: user_creds['client_secret'],
Expand Down
52 changes: 37 additions & 15 deletions spec/googleauth/get_application_default_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@
describe '#get_application_default' do
before(:example) do
@key = OpenSSL::PKey::RSA.new(2048)
@var_name = CredentialsLoader::ENV_VAR
@orig = ENV[@var_name]
@var_name = ENV_VAR
@credential_vars = [
ENV_VAR, PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR, CLIENT_ID_VAR,
CLIENT_SECRET_VAR, REFRESH_TOKEN_VAR, ACCOUNT_TYPE_VAR]
@original_env_vals = {}
@credential_vars.each { |var| @original_env_vals[var] = ENV[var] }
@home = ENV['HOME']
@scope = 'https://www.googleapis.com/auth/userinfo.profile'
end

after(:example) do
ENV[@var_name] = @orig unless @orig.nil?
@credential_vars.each { |var| ENV[var] = @original_env_vals[var] }
ENV['HOME'] = @home unless @home == ENV['HOME']
end

Expand Down Expand Up @@ -95,8 +99,7 @@
it 'succeeds with default file without GOOGLE_APPLICATION_CREDENTIALS' do
ENV.delete(@var_name) unless ENV[@var_name].nil?
Dir.mktmpdir do |dir|
key_path = File.join(dir, '.config',
CredentialsLoader::WELL_KNOWN_PATH)
key_path = File.join(dir, '.config', WELL_KNOWN_PATH)
FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, cred_json_text)
ENV['HOME'] = dir
Expand All @@ -107,8 +110,7 @@
it 'succeeds with default file without a scope' do
ENV.delete(@var_name) unless ENV[@var_name].nil?
Dir.mktmpdir do |dir|
key_path = File.join(dir, '.config',
CredentialsLoader::WELL_KNOWN_PATH)
key_path = File.join(dir, '.config', WELL_KNOWN_PATH)
FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, cred_json_text)
ENV['HOME'] = dir
Expand Down Expand Up @@ -137,17 +139,32 @@
end
stubs.verify_stubbed_calls
end

it 'succeeds if GOOGLE_PRIVATE_KEY and GOOGLE_CLIENT_EMAIL env vars are'\
' valid' do
ENV.delete(@var_name) unless ENV[@var_name].nil? # no env var
ENV[PRIVATE_KEY_VAR] = cred_json[:private_key]
ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email]
ENV[CLIENT_ID_VAR] = cred_json[:client_id]
ENV[CLIENT_SECRET_VAR] = cred_json[:client_secret]
ENV[REFRESH_TOKEN_VAR] = cred_json[:refresh_token]
ENV[ACCOUNT_TYPE_VAR] = cred_json[:type]
expect(Google::Auth.get_application_default(@scope)).to_not be_nil
end
end

describe 'when credential type is service account' do
def cred_json_text
cred_json = {
let(:cred_json) do
{
private_key_id: 'a_private_key_id',
private_key: @key.to_pem,
client_email: 'app@developer.gserviceaccount.com',
client_id: 'app.apps.googleusercontent.com',
type: 'service_account'
}
end

def cred_json_text
MultiJson.dump(cred_json)
end

Expand All @@ -156,13 +173,16 @@ def cred_json_text
end

describe 'when credential type is authorized_user' do
def cred_json_text
cred_json = {
let(:cred_json) do
{
client_secret: 'privatekey',
refresh_token: 'refreshtoken',
client_id: 'app.apps.googleusercontent.com',
type: 'authorized_user'
}
end

def cred_json_text
MultiJson.dump(cred_json)
end

Expand All @@ -171,13 +191,16 @@ def cred_json_text
end

describe 'when credential type is unknown' do
def cred_json_text
cred_json = {
let(:cred_json) do
{
client_secret: 'privatekey',
refresh_token: 'refreshtoken',
client_id: 'app.apps.googleusercontent.com',
type: 'not_known_type'
}
end

def cred_json_text
MultiJson.dump(cred_json)
end

Expand All @@ -197,8 +220,7 @@ def cred_json_text
it 'fails if the well known file contains the creds' do
ENV.delete(@var_name) unless ENV[@var_name].nil?
Dir.mktmpdir do |dir|
key_path = File.join(dir, '.config',
CredentialsLoader::WELL_KNOWN_PATH)
key_path = File.join(dir, '.config', WELL_KNOWN_PATH)
FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, cred_json_text)
ENV['HOME'] = dir
Expand Down
Loading

0 comments on commit 490278e

Please sign in to comment.