Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #12 from dlindahl/issues/update_google_drive

Update google_drive to support new OAuth API
  • Loading branch information...
commit ad9b16dbc4f2e4e4ec9163bb9d19eeef2822aa0c 2 parents 3602aa6 + 71d195f
@nesquena authored
View
10 .travis.yml
@@ -1,13 +1,9 @@
# http://about.travis-ci.org/docs/user/build-configuration/
before_script: "git submodule update --init"
rvm:
- - 1.8.7
- - 1.9.2
- - 1.9.3
- - rbx-19mode
-matrix:
- allow_failures:
- - rvm: 1.8.7
+ - 2.0.0
+ - 2.1.6
+ - 2.2.2
notifications:
recipients:
- nesquena@gmail.com
View
107 README.md
@@ -49,8 +49,8 @@ The mapper describes the column mappings and transformations to turn a spreadshe
a mapper to any worksheet (collection):
```ruby
-# Login and access a particular spreadsheet by key
-sheet = SheetMapper::Spreadsheet.new(:mapper=>SomeMapper, :key=>'k', :login => 'u', :password => 'p')
+# Access a particular spreadsheet by key
+sheet = SheetMapper::Spreadsheet.new(:mapper=>SomeMapper, :key=>'k', :session => YourGoogleSession)
# Find a particular worksheet (collection) by title
collection = sheet.find_collection_by_title('title')
# Iterate over the records within the worksheet
@@ -90,6 +90,109 @@ Use the 'cell' method to access arbitrary data points:
collection.cell(1, 2) => "foo"
```
+## Setting up OAuth 2.0
+
+To start, [follow the instructions](https://developers.google.com/console/help/new/?hl=en_US#setting-up-oauth-20) provided by Google for enabling OAuth 2.0 integration with your Google Drive account.
+
+#### Service account, Web application, or installed application?
+
+If you are building an application that integrates with a known set of
+documents, it is recommended to create your Oauth credentials as a "Service
+account". This will generate a re-usable [PKCS key](https://en.wikipedia.org/wiki/PKCS_12).
+That means that once you authenticate the first time, no further action will be
+required on your part to maintain secure integration with Google.
+
+You can also choose the "installed application" (aka native application)
+credentials, but this requires that you access a specially crafted URL and enter
+in a authentication code each time your session times out. If you are building
+a CLI tool, this becomes tedious. If you are building a customer-facing
+application, it adds friction to the process.
+
+If you are building a web application that provides integration to your
+*customers* Google Drive, that is, unfortunately, not yet supported.
+
+### Authenticating with Google Drive
+
+#### Service Account
+
+First, make note of the Client ID and Email address (service address) that
+Google's OAuth Credentials generates for you. You can find these values under
+the "APIs & auth -> Credentials" menu of the Google Developers Console which can
+be reached at https://console.developers.google.com/project/[YOUR-APPLICATION-NAME]/apiui/credential
+
+Second, store your PKCS key somewhere on your server that is secure and
+accessible to your application's Ruby process. Make note of that path.
+
+In your application code:
+
+```ruby
+google_api = SheetMapper::ApiClient.new
+google_api.service_login(YOUR_CLIENT_ID, YOUR_SERVICE_ADDRESS, PATH_TO_YOUR_PKCS_KEY)
+```
+
+#### Installed/Native Application
+
+First, make note of the Client ID and Client secret that Google's OAuth
+Credentials generates for you. You can find these values under the
+"APIs & auth -> Credentials" menu of the Google Developers Console which can be
+reached at https://console.developers.google.com/project/[YOUR-APPLICATION-NAME]/apiui/credential
+
+In your application code:
+
+```ruby
+google_api = SheetMapper::ApiClient.new
+google_api.native_login(YOUR_OAUTH_ID, YOUR_CLIENT_SECRET)
+```
+
+Calling `SheetMapper::ApiClient#native_login` will prompt you via `STDIN` to
+open a specific URL and to enter the Auth Code that Google generates for you.
+
+This will authenticate your application for the life of the current session.
+
+##### Re-using an Installed/Native Application OAuth Session
+
+Once successfully authentication. you can re-use an OAuth session by persisting
+your session's Refresh Token somewhere like the file system or in your database:
+
+```ruby
+open(PATH_TO_REFRESH_TOKEN, 'w', 0600) do |f|
+ f.puts(google_api.refresh_token)
+end
+```
+
+Then, when requesting access to your Google Drive account in a new session:
+
+```ruby
+cached_refresh_token = File.open(PATH_TO_REFRESH_TOKEN, &:gets).chomp if File.exists?(PATH_TO_REFRESH_TOKEN)
+google_api = SheetMapper::ApiClient.new
+google_api.native_login(YOUR_OAUTH_ID, YOUR_CLIENT_SECRET, YOUR_REFRESH_TOKEN, cached_refresh_token)
+```
+
+Google indicates that the refresh token is good for 1-hour. If the token is
+fresh, then no prompt will be issued to open a URL or enter in any additional
+information.
+
+If the token is stale, you will be prompted to re-authenticate your application
+by opening the URL and entering in a new authentication code.
+
+#### Web Application
+
+Unfortunately, this method of OAuth authentication is not yet supported.
+
+### Accessing Your Sheets
+
+Once you have a successfully authentication Google Drive session, you can access
+your sheets by passing your session into SheetMapper:
+
+```ruby
+sheet = SheetMapper::Spreadsheet.new(
+ mapper: SomeMapper,
+ key: SpreadsheetKey,
+ session: google_api.session
+)
+sheet.find_collection_by_title('worksheet title')
+```
+
## Contributors
SheetMapper was created by [Nathan Esquenazi](http://github.com/nesquena) at Miso in 2012. The following users
View
3  lib/sheet_mapper.rb
@@ -2,8 +2,9 @@
require 'core_ext/hash_ext' unless Hash.method_defined?(:symbolize_keys)
require 'core_ext/object_ext' unless Object.method_defined?(:present?)
-require 'google_drive_v0'
+require 'google_drive'
require 'sheet_mapper/version'
+require 'sheet_mapper/api_client'
require 'sheet_mapper/collection'
require 'sheet_mapper/spreadsheet'
require 'sheet_mapper/base'
View
91 lib/sheet_mapper/api_client.rb
@@ -0,0 +1,91 @@
+module SheetMapper
+ class ApiClient
+ KEY_PASSWORD = 'notasecret'
+ REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob"
+ SCOPE = [
+ "https://www.googleapis.com/auth/drive",
+ "https://spreadsheets.google.com/feeds/"
+ ]
+ TOKEN_CREDENTIAL_URI = 'https://accounts.google.com/o/oauth2/token'
+ class LoginFailure < StandardError; end
+
+ def initialize(options = {})
+ options[:app_name] ||= 'SheetMapper'
+ options[:app_version] ||= SheetMapper::VERSION
+ @app_name, @app_version = options[:app_name], options[:app_version]
+ end
+
+ def authorization_uri
+ @authorization_client.authorization_uri
+ end
+
+ def native_login(client_id, client_secret, refresh_token = nil)
+ @authorization_client = native_auth_client(client_id, client_secret)
+ if refresh_token
+ print "\nAttempting to reuse previously saved session..."
+ @authorization_client.refresh_token = refresh_token
+ else
+ @authorization_client.code = prompt_for_auth_code
+ end
+ begin
+ @authorization_client.fetch_access_token!
+ rescue Signet::AuthorizationError
+ raise unless refresh_token
+ print " Failed!\n"
+ native_login(client_id, client_secret)
+ else
+ print " Success!\n" if refresh_token
+ end
+ end
+
+ def refresh_token
+ @authorization_client.refresh_token
+ end
+
+ def service_login(client_id, client_email, p12_path)
+ key = Google::APIClient::KeyUtils.load_from_pkcs12(p12_path, KEY_PASSWORD)
+ @authorization_client = service_auth_client(client_email, key)
+ @authorization_client.fetch_access_token!
+ end
+
+ def session
+ @session ||= begin
+ fail LoginFailure, "You must execute a login method" if @authorization_client.nil?
+ @session = GoogleDrive.login_with_oauth(@authorization_client.access_token)
+ end
+ end
+
+ private
+
+ def native_auth_client(client_id, client_secret)
+ client = Google::APIClient.new(application_name: @app_name, application_version: @app_version)
+ auth = client.authorization
+ auth.client_id = client_id
+ auth.client_secret = client_secret
+ auth.redirect_uri = REDIRECT_URI
+ auth.scope = SCOPE
+ auth
+ end
+
+ def prompt_for_auth_code
+ print "\n"
+ print("1. Open your favorite web browser\n")
+ print("2. Sign in to your Google account\n")
+ print("3. Open the following URL:\n\n%s\n\n" % authorization_uri)
+ print("4. Click \"Accept\" to grant this application access to your Google Drive\n")
+ print("5. Enter the authorization code shown in the page: ")
+ STDIN.gets.chomp
+ end
+
+ def service_auth_client(client_email, key)
+ Signet::OAuth2::Client.new(
+ token_credential_uri: TOKEN_CREDENTIAL_URI,
+ audience: TOKEN_CREDENTIAL_URI,
+ scope: SCOPE,
+ issuer: client_email,
+ access_type: 'offline',
+ signing_key: key
+ )
+ end
+ end
+end
View
6 lib/sheet_mapper/spreadsheet.rb
@@ -4,8 +4,8 @@ class Spreadsheet
# SheetMapper::Worksheet.new(:mapper => SomeMapper, :key => 'sheet_key', :login => 'user', :password => 'pass')
def initialize(options={})
- @mapper = options[:mapper]
- @session = ::GoogleDriveV0.login(options[:login], options[:password])
+ @mapper = options[:mapper]
+ @session = options[:session]
@spreadsheet = find_spreadsheet(options[:key], options[:url], options[:title])
end
@@ -27,4 +27,4 @@ def find_spreadsheet(key, url, title)
end
end
-end
+end
View
2  sheet_mapper.gemspec
@@ -23,7 +23,7 @@ Gem::Specification.new do |s|
s.add_development_dependency 'minitest', "~> 2.11.0"
s.add_development_dependency 'rake'
- s.add_development_dependency 'mocha'
+ s.add_development_dependency 'mocha', '~> 1.1'
s.add_development_dependency 'fakeweb'
s.add_development_dependency 'awesome_print'
end
View
101 test/api_client_test.rb
@@ -0,0 +1,101 @@
+require File.expand_path('../test_config.rb', __FILE__)
+
+describe "ApiClient" do
+ setup do
+ @client = SheetMapper::ApiClient.new
+ end
+
+ context "#native_login" do
+ setup do
+ @fake_client = mock('fake_native_client')
+ @client.stubs(:native_auth_client).returns(@fake_client)
+ end
+
+ it "fetches the access token from a user-entered auth code" do
+ client_id = "YOUR CLIENT ID"
+ client_secret = "YOUR CLIENT SECRET"
+ @client.stubs(:prompt_for_auth_code).returns('AUTH_CODE')
+ @fake_client.expects('code=').returns('AUTH_CODE')
+ @fake_client.expects('fetch_access_token!')
+ @client.native_login(client_id, client_secret)
+ end
+
+ context "with a fresh refresh token" do
+ it "reuses the previous session" do
+ client_id = "YOUR CLIENT ID"
+ client_secret = "YOUR CLIENT SECRET"
+ refresh_token = "YOUR REFRESH TOKEN"
+ @fake_client.expects('refresh_token=').returns(refresh_token)
+ @fake_client.expects('fetch_access_token!')
+ @client.expects(:print).at_least_once
+ @client.native_login(client_id, client_secret, refresh_token)
+ end
+ end
+ end
+
+ context "#service_login" do
+ setup do
+ @fake_client = mock('fake_native_client')
+ @client.stubs(:service_auth_client).returns(@fake_client)
+ end
+
+ it "uses the P12 key to fetch the access token" do
+ client_id = "YOUR CLIENT ID"
+ client_email = "client@example.com"
+ key_path = "PATH_TO_KEY"
+ Google::APIClient::KeyUtils.expects(:load_from_pkcs12).with(key_path, 'notasecret').returns('KEY')
+ @fake_client.expects('fetch_access_token!')
+ @client.service_login(client_id, client_email, key_path)
+ end
+ end
+
+ context "#session" do
+ context "without being logged in" do
+ it "raises an error" do
+ assert_raises SheetMapper::ApiClient::LoginFailure do
+ @client.session
+ end
+ end
+ end
+
+ context "with a logged in session" do
+ setup do
+ @fake_client = mock('fake_client', access_token:'ACCESS_TOKEN')
+ @client.instance_variable_set('@authorization_client', @fake_client)
+ end
+
+ it "returns a GoogleDrive API instance" do
+ @google_api = mock('google_api')
+ GoogleDrive.expects(:login_with_oauth).with('ACCESS_TOKEN').returns(@google_api)
+ assert_equal @google_api, @client.session
+ end
+ end
+ end
+
+ # context "authorization" do
+ # should "return a OAuth2 Client" do
+ # assert_kind_of Signet::OAuth2::Client, @client.authorization
+ # end
+ # end
+
+ # context "session" do
+ # setup do
+ # @client.expects(:get_token!).returns(123)
+ # end
+
+ # should "returns a GoogleDrive::Session" do
+ # assert_kind_of GoogleDrive::Session, @client.session
+ # end
+ # end
+
+ # context "prompt_for_auth_code" do
+ # setup do
+ # @client.expects(:print).twice
+ # STDIN.expects(:gets).returns('AUTH_CODE')
+ # end
+
+ # should "prompt the user to enter a code retrieved from a URI" do
+ # assert_equal 'AUTH_CODE', @client.prompt_for_auth_code
+ # end
+ # end
+end
View
5 test/spreadsheet_test.rb
@@ -4,14 +4,13 @@
setup do
@sheet_stub = stub(:sheet)
@session_stub = stub(:session)
- ::GoogleDrive.expects(:login).with('login', 'pass').returns(@session_stub)
end
[:key, :url, :title].each do |identifier|
context "for initialize by #{identifier}" do
setup do
@session_stub.expects(:"spreadsheet_by_#{identifier}").with('foo').returns(@sheet_stub)
- @sheet = SheetMapper::Spreadsheet.new(:mapper => Object, identifier => 'foo', :login => 'login', :password => 'pass')
+ @sheet = SheetMapper::Spreadsheet.new(:mapper => Object, :session => @session_stub, identifier => 'foo')
end
should "not return spreadsheet class" do
@@ -24,7 +23,7 @@
context "for find_collection_by_title method" do
setup do
@session_stub.expects(:spreadsheet_by_key).with('foo').returns(@sheet_stub)
- @sheet = SheetMapper::Spreadsheet.new(:mapper => Object, :key => 'foo', :login => 'login', :password => 'pass')
+ @sheet = SheetMapper::Spreadsheet.new(:mapper => Object, :key => 'foo', session: @session_stub)
@work_stub = stub(:worksheet)
@work_stub.expects(:title).returns("FOO")
@sheet_stub.expects(:worksheets).returns([@work_stub])
View
7 test/test_config.rb
@@ -1,12 +1,9 @@
require 'minitest/autorun'
-require 'mocha'
+require 'mocha/mini_test'
require 'fakeweb'
require 'ap'
require File.expand_path('../../lib/sheet_mapper', __FILE__)
Dir[File.expand_path("../test_helpers/*.rb", __FILE__)].each { |f| require f }
FakeWeb.allow_net_connect = false
-
-class MiniTest::Unit::TestCase
- include Mocha::API
-end
+Mocha::Configuration.prevent(:stubbing_non_existent_method)

0 comments on commit ad9b16d

Please sign in to comment.
Something went wrong with that request. Please try again.