diff --git a/.gitignore b/.gitignore index 67599524b..fb4875a9a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,10 @@ pkg specdoc wiki .google-api.yaml +*.log + +#IntelliJ +.idea +*.iml +atlassian* + diff --git a/lib/google/api_client/auth/file_storage.rb b/lib/google/api_client/auth/file_storage.rb index fca4ea39a..1562bedf8 100644 --- a/lib/google/api_client/auth/file_storage.rb +++ b/lib/google/api_client/auth/file_storage.rb @@ -12,47 +12,39 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'json' require 'signet/oauth_2/client' +require_relative 'storage' +require_relative 'storages/file_store' module Google class APIClient + ## # Represents cached OAuth 2 tokens stored on local disk in a # JSON serialized file. Meant to resemble the serialized format # http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html # + # @deprecated + # Use {Google::APIClient::Storage} and {Google::APIClient::FileStore} instead + # class FileStorage - # @return [String] Path to the credentials file. - attr_accessor :path - # @return [Signet::OAuth2::Client] Path to the credentials file. - attr_reader :authorization + attr_accessor :storage, + :path - ## - # Initializes the FileStorage object. - # - # @param [String] path - # Path to the credentials file. def initialize(path) @path = path - self.load_credentials + store = Google::APIClient::FileStore.new(@path) + @storage = Google::APIClient::Storage.new(store) + @storage.authorize end - ## - # Attempt to read in credentials from the specified file. def load_credentials - if File.exists? self.path - File.open(self.path, 'r') do |file| - cached_credentials = JSON.load(file) - @authorization = Signet::OAuth2::Client.new(cached_credentials) - @authorization.issued_at = Time.at(cached_credentials['issued_at']) - if @authorization.expired? - @authorization.fetch_access_token! - self.write_credentials - end - end - end + storage.authorize + end + + def authorization + storage.authorization end ## @@ -61,26 +53,9 @@ module Google # @param [Signet::OAuth2::Client] authorization # Optional authorization instance. If not provided, the authorization # already associated with this instance will be written. - def write_credentials(authorization=nil) - @authorization = authorization unless authorization.nil? - - unless @authorization.refresh_token.nil? - hash = {} - %w'access_token - authorization_uri - client_id - client_secret - expires_in - refresh_token - token_credential_uri'.each do |var| - hash[var] = @authorization.instance_variable_get("@#{var}") - end - hash['issued_at'] = @authorization.issued_at.to_i - - File.open(self.path, 'w', 0600) do |file| - file.write(hash.to_json) - end - end + def write_credentials(auth=nil) + self.authorization = auth unless auth.nil? + storage.write_credentials(self.authorization) end end end diff --git a/lib/google/api_client/auth/storage.rb b/lib/google/api_client/auth/storage.rb new file mode 100644 index 000000000..e7cc5bc4e --- /dev/null +++ b/lib/google/api_client/auth/storage.rb @@ -0,0 +1,101 @@ +# Copyright 2013 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 'signet/oauth_2/client' + +module Google + class APIClient + ## + # Represents cached OAuth 2 tokens stored on local disk in a + # JSON serialized file. Meant to resemble the serialized format + # http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html + # + class Storage + + AUTHORIZATION_URI = 'https://accounts.google.com/o/oauth2/auth' + TOKEN_CREDENTIAL_URI = 'https://accounts.google.com/o/oauth2/token' + + # @return [Object] Storage object. + attr_accessor :store + + # @return [Signet::OAuth2::Client] + attr_reader :authorization + + ## + # Initializes the Storage object. + # + # @params [Object] Storage object + def initialize(store) + @store= store + end + + ## + # Write the credentials to the specified store. + # + # @params [Signet::OAuth2::Client] authorization + # Optional authorization instance. If not provided, the authorization + # already associated with this instance will be written. + def write_credentials(authorization=nil) + @authorization = authorization if authorization + if @authorization.respond_to?(:refresh_token) && @authorization.refresh_token + store.write_credentials(credentials_hash) + end + end + + ## + # Loads credentials and authorizes an client. + # @return [Object] Signet::OAuth2::Client or NIL + def authorize + @authorization = nil + cached_credentials = load_credentials + if cached_credentials && cached_credentials.size > 0 + @authorization = Signet::OAuth2::Client.new(cached_credentials) + @authorization.issued_at = Time.at(cached_credentials['issued_at'].to_i) + self.refresh_authorization if @authorization.expired? + end + return @authorization + end + + ## + # refresh credentials and save them to store + def refresh_authorization + authorization.refresh! + self.write_credentials + end + + private + + ## + # Attempt to read in credentials from the specified store. + def load_credentials + store.load_credentials + end + + ## + # @return [Hash] with credentials + def credentials_hash + { + :access_token => authorization.access_token, + :authorization_uri => AUTHORIZATION_URI, + :client_id => authorization.client_id, + :client_secret => authorization.client_secret, + :expires_in => authorization.expires_in, + :refresh_token => authorization.refresh_token, + :token_credential_uri => TOKEN_CREDENTIAL_URI, + :issued_at => authorization.issued_at.to_i + } + end + end + end +end diff --git a/lib/google/api_client/auth/storages/file_store.rb b/lib/google/api_client/auth/storages/file_store.rb new file mode 100644 index 000000000..cd3eae710 --- /dev/null +++ b/lib/google/api_client/auth/storages/file_store.rb @@ -0,0 +1,58 @@ +# Copyright 2013 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 'json' + +module Google + class APIClient + ## + # Represents cached OAuth 2 tokens stored on local disk in a + # JSON serialized file. Meant to resemble the serialized format + # http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html + # + class FileStore + + attr_accessor :path + + ## + # Initializes the FileStorage object. + # + # @param [String] path + # Path to the credentials file. + def initialize(path) + @path= path + end + + ## + # Attempt to read in credentials from the specified file. + def load_credentials + open(path, 'r') { |f| JSON.parse(f.read) } + rescue + nil + end + + ## + # Write the credentials to the specified file. + # + # @param [Signet::OAuth2::Client] authorization + # Optional authorization instance. If not provided, the authorization + # already associated with this instance will be written. + def write_credentials(credentials_hash) + open(self.path, 'w+') do |f| + f.write(credentials_hash.to_json) + end + end + end + end +end diff --git a/lib/google/api_client/auth/storages/redis_store.rb b/lib/google/api_client/auth/storages/redis_store.rb new file mode 100644 index 000000000..3f76f7ca8 --- /dev/null +++ b/lib/google/api_client/auth/storages/redis_store.rb @@ -0,0 +1,54 @@ +# Copyright 2013 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 'json' + +module Google + class APIClient + class RedisStore + + DEFAULT_REDIS_CREDENTIALS_KEY = "google_api_credentials" + + attr_accessor :redis + + ## + # Initializes the RedisStore object. + # + # @params [Object] Redis instance + def initialize(redis, key = nil) + @redis= redis + @redis_credentials_key = key + end + + ## + # Attempt to read in credentials from redis. + def load_credentials + credentials = redis.get redis_credentials_key + JSON.parse(credentials) if credentials + end + + def redis_credentials_key + @redis_credentials_key || DEFAULT_REDIS_CREDENTIALS_KEY + end + + ## + # Write the credentials to redis. + # + # @params [Hash] credentials + def write_credentials(credentials_hash) + redis.set(redis_credentials_key, credentials_hash.to_json) + end + end + end +end diff --git a/lib/google/api_client/service_account.rb b/lib/google/api_client/service_account.rb index b6a0b3cb0..3d941ae07 100644 --- a/lib/google/api_client/service_account.rb +++ b/lib/google/api_client/service_account.rb @@ -16,3 +16,6 @@ require 'google/api_client/auth/pkcs12' require 'google/api_client/auth/jwt_asserter' require 'google/api_client/auth/key_utils' require 'google/api_client/auth/compute_service_account' +require 'google/api_client/auth/storage' +require 'google/api_client/auth/storages/redis_store' +require 'google/api_client/auth/storages/file_store' diff --git a/spec/fixtures/files/auth_stored_credentials.json b/spec/fixtures/files/auth_stored_credentials.json new file mode 100644 index 000000000..4cd786e4a --- /dev/null +++ b/spec/fixtures/files/auth_stored_credentials.json @@ -0,0 +1,8 @@ +{ "access_token":"access_token_123456789", + "authorization_uri":"https://accounts.google.com/o/oauth2/auth", + "client_id":"123456789p.apps.googleusercontent.com", + "client_secret":"very_secret", + "expires_in":3600, + "refresh_token":"refresh_token_12345679", + "token_credential_uri":"https://accounts.google.com/o/oauth2/token", + "issued_at":1386053761} \ No newline at end of file diff --git a/spec/google/api_client/auth/storage_spec.rb b/spec/google/api_client/auth/storage_spec.rb new file mode 100644 index 000000000..d8e5b960c --- /dev/null +++ b/spec/google/api_client/auth/storage_spec.rb @@ -0,0 +1,122 @@ +require 'spec_helper' + +require 'google/api_client' +require 'google/api_client/version' + +describe Google::APIClient::Storage do + let(:client) { Google::APIClient.new(:application_name => 'API Client Tests') } + let(:root_path) { File.expand_path(File.join(__FILE__, '..', '..', '..')) } + let(:json_file) { File.expand_path(File.join(root_path, 'fixtures', 'files', 'auth_stored_credentials.json')) } + + let(:store) { double } + let(:client_stub) { double } + subject { Google::APIClient::Storage.new(store) } + + describe 'authorize' do + it 'should authorize' do + expect(subject).to respond_to(:authorization) + expect(subject.store).to be == store + end + end + + describe 'authorize' do + describe 'with credentials' do + + it 'should initialize a new OAuth Client' do + expect(subject).to receive(:load_credentials).and_return({:first => 'a dummy'}) + expect(client_stub).to receive(:issued_at=) + expect(client_stub).to receive(:expired?).and_return(false) + expect(Signet::OAuth2::Client).to receive(:new).and_return(client_stub) + expect(subject).not_to receive(:refresh_authorization) + subject.authorize + end + + it 'should refresh authorization' do + expect(subject).to receive(:load_credentials).and_return({:first => 'a dummy'}) + expect(client_stub).to receive(:issued_at=) + expect(client_stub).to receive(:expired?).and_return(true) + expect(Signet::OAuth2::Client).to receive(:new).and_return(client_stub) + expect(subject).to receive(:refresh_authorization) + auth = subject.authorize + expect(auth).to be == subject.authorization + expect(auth).not_to be_nil + end + end + + describe 'without credentials' do + + it 'should return nil' do + expect(subject.authorization).to be_nil + expect(subject).to receive(:load_credentials).and_return({}) + expect(subject.authorize).to be_nil + expect(subject.authorization).to be_nil + end + end + end + + describe 'write_credentials' do + it 'should call store to write credentials' do + authorization_stub = double + expect(authorization_stub).to receive(:refresh_token).and_return(true) + expect(subject).to receive(:credentials_hash) + expect(subject.store).to receive(:write_credentials) + subject.write_credentials(authorization_stub) + expect(subject.authorization).to be == authorization_stub + end + + it 'should not call store to write credentials' do + expect(subject).not_to receive(:credentials_hash) + expect(subject.store).not_to receive(:write_credentials) + expect { + subject.write_credentials() + }.not_to raise_error + end + it 'should not call store to write credentials' do + expect(subject).not_to receive(:credentials_hash) + expect(subject.store).not_to receive(:write_credentials) + expect { + subject.write_credentials('something') + }.not_to raise_error + end + + end + + describe 'refresh_authorization' do + it 'should call refresh and write credentials' do + expect(subject).to receive(:write_credentials) + authorization_stub = double + expect(subject).to receive(:authorization).and_return(authorization_stub) + expect(authorization_stub).to receive(:refresh!).and_return(true) + subject.refresh_authorization + end + end + + describe 'load_credentials' do + it 'should call store to load credentials' do + expect(subject.store).to receive(:load_credentials) + subject.send(:load_credentials) + end + end + + describe 'credentials_hash' do + it 'should return an hash' do + authorization_stub = double + expect(authorization_stub).to receive(:access_token) + expect(authorization_stub).to receive(:client_id) + expect(authorization_stub).to receive(:client_secret) + expect(authorization_stub).to receive(:expires_in) + expect(authorization_stub).to receive(:refresh_token) + expect(authorization_stub).to receive(:issued_at).and_return('100') + allow(subject).to receive(:authorization).and_return(authorization_stub) + credentials = subject.send(:credentials_hash) + expect(credentials).to include(:access_token) + expect(credentials).to include(:authorization_uri) + expect(credentials).to include(:client_id) + expect(credentials).to include(:client_secret) + expect(credentials).to include(:expires_in) + expect(credentials).to include(:refresh_token) + expect(credentials).to include(:token_credential_uri) + expect(credentials).to include(:issued_at) + end + end +end diff --git a/spec/google/api_client/auth/storages/file_store_spec.rb b/spec/google/api_client/auth/storages/file_store_spec.rb new file mode 100644 index 000000000..2963b1d45 --- /dev/null +++ b/spec/google/api_client/auth/storages/file_store_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +require 'google/api_client' +require 'google/api_client/version' + +describe Google::APIClient::FileStore do + let(:root_path) { File.expand_path(File.join(__FILE__, '..','..','..', '..','..')) } + let(:json_file) { File.expand_path(File.join(root_path, 'fixtures', 'files', 'auth_stored_credentials.json')) } + + let(:credentials_hash) {{ + "access_token"=>"my_access_token", + "authorization_uri"=>"https://accounts.google.com/o/oauth2/auth", + "client_id"=>"123456_test_client_id@.apps.googleusercontent.com", + "client_secret"=>"123456_client_secret", + "expires_in"=>3600, + "refresh_token"=>"my_refresh_token", + "token_credential_uri"=>"https://accounts.google.com/o/oauth2/token", + "issued_at"=>1384440275 + }} + + subject{Google::APIClient::FileStore.new('a file path')} + + it 'should have a path' do + expect(subject.path).to be == 'a file path' + subject.path = 'an other file path' + expect(subject.path).to be == 'an other file path' + end + + it 'should load credentials' do + subject.path = json_file + credentials = subject.load_credentials + expect(credentials).to include('access_token', 'authorization_uri', 'refresh_token') + end + + it 'should write credentials' do + io_stub = StringIO.new + expect(subject).to receive(:open).and_return(io_stub) + subject.write_credentials(credentials_hash) + end +end diff --git a/spec/google/api_client/auth/storages/redis_store_spec.rb b/spec/google/api_client/auth/storages/redis_store_spec.rb new file mode 100644 index 000000000..de5abc4a1 --- /dev/null +++ b/spec/google/api_client/auth/storages/redis_store_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +require 'google/api_client' +require 'google/api_client/version' + + +describe Google::APIClient::RedisStore do + let(:root_path) { File.expand_path(File.join(__FILE__, '..', '..', '..', '..', '..')) } + let(:json_file) { File.expand_path(File.join(root_path, 'fixtures', 'files', 'auth_stored_credentials.json')) } + let(:redis) {double} + + let(:credentials_hash) { { + "access_token" => "my_access_token", + "authorization_uri" => "https://accounts.google.com/o/oauth2/auth", + "client_id" => "123456_test_client_id@.apps.googleusercontent.com", + "client_secret" => "123456_client_secret", + "expires_in" => 3600, + "refresh_token" => "my_refresh_token", + "token_credential_uri" => "https://accounts.google.com/o/oauth2/token", + "issued_at" => 1384440275 + } } + + subject { Google::APIClient::RedisStore.new('a redis instance') } + + it 'should have a redis instance' do + expect(subject.redis).to be == 'a redis instance' + subject.redis = 'an other redis instance' + expect(subject.redis).to be == 'an other redis instance' + end + + describe 'load_credentials' do + + it 'should load credentials' do + subject.redis= redis + expect(redis).to receive(:get).and_return(credentials_hash.to_json) + expect(subject.load_credentials).to be == credentials_hash + end + + it 'should return nil' do + subject.redis= redis + expect(redis).to receive(:get).and_return(nil) + expect(subject.load_credentials).to be_nil + end + end + + describe 'redis_credentials_key' do + context 'without given key' do + it 'should return default key' do + expect(subject.redis_credentials_key).to be == "google_api_credentials" + end + end + context 'with given key' do + let(:redis_store) { Google::APIClient::RedisStore.new('a redis instance', 'another_google_api_credentials') } + it 'should use given key' do + expect(redis_store.redis_credentials_key).to be == "another_google_api_credentials" + end + end + + end + + describe 'write credentials' do + + it 'should write credentials' do + subject.redis= redis + expect(redis).to receive(:set).and_return('ok') + expect(subject.write_credentials(credentials_hash)).to be_truthy + end + end + +end