Enables reading credentials from env vars.

- 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.
This commit is contained in:
Herbert Siojo 2015-05-21 13:38:19 -07:00
parent 133e05cf4a
commit 5061fb5add
9 changed files with 227 additions and 71 deletions

View File

@ -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
@ -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

View File

@ -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
@ -20,3 +25,4 @@
[@tbetbetbe]: https://github.com/tbetbetbe
[@joneslee85]: https://github.com/joneslee85
[@haabaato]: https://github.com/haabaato

View File

@ -52,16 +52,38 @@ END
# override CredentialsLoader#make_creds to use the class determined by
# loading the json.
def self.make_creds(json_key_io, scope = nil)
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(StringIO.new(MultiJson.dump(json_key)), scope)
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]

View File

@ -39,6 +39,14 @@ module Google
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'
@ -63,11 +71,14 @@ module Google
#
# @param scope [string|array|nil] the scope(s) to access
def from_env(scope = nil)
return nil unless ENV.key?(ENV_VAR)
if ENV.key?(ENV_VAR)
path = ENV[ENV_VAR]
fail 'file #{path} does not exist' unless File.exist?(path)
fail "file #{path} does not exist" unless File.exist?(path)
File.open(path) do |f|
return make_creds(f, scope)
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}"
@ -83,11 +94,22 @@ module Google
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

View File

@ -62,8 +62,15 @@ module Google
#
# @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)
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,
@ -90,7 +97,7 @@ module Google
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
@ -120,7 +127,7 @@ module Google
# 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
@ -135,8 +142,14 @@ module Google
# Initializes a ServiceAccountJwtHeaderCredentials.
#
# @param json_key_io [IO] an IO from which the JSON key can be read
def initialize(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)

View File

@ -63,8 +63,15 @@ module Google
#
# @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'],

View File

@ -38,14 +38,18 @@ require 'spec_helper'
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
@ -95,8 +99,7 @@ describe '#get_application_default' do
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
@ -107,8 +110,7 @@ describe '#get_application_default' do
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
@ -137,17 +139,31 @@ describe '#get_application_default' do
end
stubs.verify_stubbed_calls
end
it 'succeeds if environment 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
@ -156,13 +172,16 @@ describe '#get_application_default' do
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
@ -171,13 +190,16 @@ describe '#get_application_default' do
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
@ -197,8 +219,7 @@ describe '#get_application_default' do
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
@ -208,5 +229,14 @@ describe '#get_application_default' do
expect(&blk).to raise_error RuntimeError
end
end
it 'fails if env vars are set' do
ENV[PRIVATE_KEY_VAR] = cred_json[:private_key]
ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email]
blk = proc do
Google::Auth.get_application_default(@scope)
end
expect(&blk).to raise_error RuntimeError
end
end
end

View File

@ -108,12 +108,22 @@ end
describe Google::Auth::ServiceAccountCredentials do
ServiceAccountCredentials = Google::Auth::ServiceAccountCredentials
let(:client_email) { 'app@developer.gserviceaccount.com' }
let(:cred_json) do
{
private_key_id: 'a_private_key_id',
private_key: @key.to_pem,
client_email: client_email,
client_id: 'app.apps.googleusercontent.com',
type: 'service_account'
}
end
before(:example) do
@key = OpenSSL::PKey::RSA.new(2048)
@client = ServiceAccountCredentials.new(
StringIO.new(cred_json_text),
'https://www.googleapis.com/auth/userinfo.profile')
json_key_io: StringIO.new(cred_json_text),
scope: 'https://www.googleapis.com/auth/userinfo.profile'
)
end
def make_auth_stubs(opts = {})
@ -131,13 +141,6 @@ describe Google::Auth::ServiceAccountCredentials do
end
def cred_json_text
cred_json = {
private_key_id: 'a_private_key_id',
private_key: @key.to_pem,
client_email: client_email,
client_id: 'app.apps.googleusercontent.com',
type: 'service_account'
}
MultiJson.dump(cred_json)
end
@ -154,13 +157,18 @@ describe Google::Auth::ServiceAccountCredentials do
describe '#from_env' do
before(:example) do
@var_name = ENV_VAR
@orig = ENV[@var_name]
@credential_vars = [
ENV_VAR, PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR, ACCOUNT_TYPE_VAR]
@original_env_vals = {}
@credential_vars.each { |var| @original_env_vals[var] = ENV[var] }
ENV[ACCOUNT_TYPE_VAR] = cred_json[:type]
@scope = 'https://www.googleapis.com/auth/userinfo.profile'
@clz = ServiceAccountCredentials
end
after(:example) do
ENV[@var_name] = @orig unless @orig.nil?
@credential_vars.each { |var| ENV[var] = @original_env_vals[var] }
end
it 'returns nil if the GOOGLE_APPLICATION_CREDENTIALS is unset' do
@ -187,6 +195,13 @@ describe Google::Auth::ServiceAccountCredentials do
expect(@clz.from_env(@scope)).to_not be_nil
end
end
it 'succeeds when GOOGLE_PRIVATE_KEY and GOOGLE_CLIENT_EMAIL env vars are'\
' valid' do
ENV[PRIVATE_KEY_VAR] = cred_json[:private_key]
ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email]
expect(@clz.from_env(@scope)).to_not be_nil
end
end
describe '#from_well_known_path' do
@ -224,20 +239,22 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do
let(:client_email) { 'app@developer.gserviceaccount.com' }
let(:clz) { Google::Auth::ServiceAccountJwtHeaderCredentials }
before(:example) do
@key = OpenSSL::PKey::RSA.new(2048)
@client = clz.new(StringIO.new(cred_json_text))
end
def cred_json_text
cred_json = {
let(:cred_json) do
{
private_key_id: 'a_private_key_id',
private_key: @key.to_pem,
client_email: client_email,
client_id: 'app.apps.googleusercontent.com',
type: 'service_account'
}
end
before(:example) do
@key = OpenSSL::PKey::RSA.new(2048)
@client = clz.new(json_key_io: StringIO.new(cred_json_text))
end
def cred_json_text
MultiJson.dump(cred_json)
end
@ -246,11 +263,15 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do
describe '#from_env' do
before(:example) do
@var_name = ENV_VAR
@orig = ENV[@var_name]
@credential_vars = [
ENV_VAR, PRIVATE_KEY_VAR, CLIENT_EMAIL_VAR, ACCOUNT_TYPE_VAR]
@original_env_vals = {}
@credential_vars.each { |var| @original_env_vals[var] = ENV[var] }
ENV[ACCOUNT_TYPE_VAR] = cred_json[:type]
end
after(:example) do
ENV[@var_name] = @orig unless @orig.nil?
@credential_vars.each { |var| ENV[var] = @original_env_vals[var] }
end
it 'returns nil if the GOOGLE_APPLICATION_CREDENTIALS is unset' do
@ -277,6 +298,13 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do
expect(clz.from_env).to_not be_nil
end
end
it 'succeeds when GOOGLE_PRIVATE_KEY and GOOGLE_CLIENT_EMAIL env vars are'\
' valid' do
ENV[PRIVATE_KEY_VAR] = cred_json[:private_key]
ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email]
expect(clz.from_env(@scope)).to_not be_nil
end
end
describe '#from_well_known_path' do

View File

@ -40,15 +40,26 @@ require 'openssl'
require 'spec_helper'
require 'tmpdir'
include Google::Auth::CredentialsLoader
describe Google::Auth::UserRefreshCredentials do
UserRefreshCredentials = Google::Auth::UserRefreshCredentials
CredentialsLoader = Google::Auth::CredentialsLoader
let(:cred_json) do
{
client_secret: 'privatekey',
client_id: 'client123',
refresh_token: 'refreshtoken',
type: 'authorized_user'
}
end
before(:example) do
@key = OpenSSL::PKey::RSA.new(2048)
@client = UserRefreshCredentials.new(
StringIO.new(cred_json_text),
'https://www.googleapis.com/auth/userinfo.profile')
json_key_io: StringIO.new(cred_json_text),
scope: 'https://www.googleapis.com/auth/userinfo.profile'
)
end
def make_auth_stubs(opts = {})
@ -64,12 +75,6 @@ describe Google::Auth::UserRefreshCredentials do
end
def cred_json_text(missing = nil)
cred_json = {
client_secret: 'privatekey',
client_id: 'client123',
refresh_token: 'refreshtoken',
type: 'authorized_user'
}
cred_json.delete(missing.to_sym) unless missing.nil?
MultiJson.dump(cred_json)
end
@ -78,14 +83,18 @@ describe Google::Auth::UserRefreshCredentials do
describe '#from_env' do
before(:example) do
@var_name = CredentialsLoader::ENV_VAR
@orig = ENV[@var_name]
@var_name = ENV_VAR
@credential_vars = [
ENV_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] }
@scope = 'https://www.googleapis.com/auth/userinfo.profile'
@clz = UserRefreshCredentials
end
after(:example) do
ENV[@var_name] = @orig unless @orig.nil?
@credential_vars.each { |var| ENV[var] = @original_env_vals[var] }
end
it 'returns nil if the GOOGLE_APPLICATION_CREDENTIALS is unset' do
@ -125,13 +134,22 @@ describe Google::Auth::UserRefreshCredentials do
expect(@clz.from_env(@scope)).to_not be_nil
end
end
it 'succeeds when GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and '\
'GOOGLE_REFRESH_TOKEN env vars are valid' do
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(@clz.from_env(@scope)).to_not be_nil
end
end
describe '#from_well_known_path' do
before(:example) do
@home = ENV['HOME']
@scope = 'https://www.googleapis.com/auth/userinfo.profile'
@known_path = CredentialsLoader::WELL_KNOWN_PATH
@known_path = WELL_KNOWN_PATH
@clz = UserRefreshCredentials
end
@ -152,7 +170,7 @@ describe Google::Auth::UserRefreshCredentials do
FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, cred_json_text(missing))
ENV['HOME'] = dir
expect { @clz.from_env(@scope) }.to raise_error
expect { @clz.from_well_known_path(@scope) }.to raise_error
end
end
end