Tag Archives: Marketplace ISV Guest

Building a Rails based app for Google Apps Marketplace

Editor's note: This is a guest post by Benjamin Coe. Benjamin shares tips and best practices on using Ruby on Rails for integrating with Google Apps and launching on the Marketplace. — Arun Nagarajan

Yesware offers an app in the Google Apps Marketplace which allows our users to schedule reminders, from directly within the Gmail UI. Yesware’s app recently relaunched in the updated Google Apps Marketplace. In prep, we revamped our existing Google Apps Integration:
  • Replacing OpenID with OAuth 2.0 for Single-Sign-On.
  • Replacing 2-legged OAuth with OAuth 2.0 Service Accounts, for delegated account access.
  • Releasing a Gmail Contextual Gadget that worked within these new authentication paradigms.
We should like to share some of the decisions we made, and challenges we faced, upgrading our production Ruby on Rails application to support the improved Google Apps Marketplace.

OAuth 2.0 for SSO

In the revamped Google Apps Marketplace, OAuth 2.0 replaces OpenID for facilitating Single-Sign-On. The flow is as follows:

  1. OAuth 2.0 credentials are created in the Cloud Console, within the same project that has the Google Apps Marketplace SDK enabled.
  2. When accessing your application, a user is put through the standard OAuth 2.0 authentication flow using these credentials.
  3. If the user has the Google Apps Marketplace App installed they will be logged directly into your application, skipping the authorization step.
To implement the OAuth 2.0 authentication flow, you can use the OmniAuth Google OAuth2 Strategy gem. Assuming you're already using OmniAuth, you simply add a line to initializers/omniauth.rb that looks something like this:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV["GAM_OAUTH_KEY"], ENV["GAM_OAUTH_SECRET"]
end

Yesware already had a Google OAuth 2.0 authentication strategy, so we opted to subclass the Google OAuth 2.0 OmniAuth Strategy. This allowed us to continue supporting our existing OAuth 2.0 credentials, while adding support for Google Apps Marketplace SSO. Our subclassed strategy looked like this:
# Subclass the GoogleOauth2 Omniauth strategy for
# Google Apps Marketplace V2 SSO.
module OmniAuth
module Strategies
class GoogleAppsMarketplace < OmniAuth::Strategies::GoogleOauth2
option :name, 'google_apps_marketplace'
end
end
end
Our final initializers/omniauth.rb file was this:
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV["OAUTH_KEY"],
ENV["OAUTH_SECRET"],
{:scope => ENV["OAUTH_SCOPE"]}
provider :google_apps_marketplace, ENV["GAM_OAUTH_KEY"],
ENV["GAM_OAUTH_SECRET"],
{ :scope => ENV["GAM_OAUTH_SCOPE"],
:access_type => 'online' }end

Note that :access_type is set to online. This is necessary to prevent the authorization prompt from being presented to a SSO user. Omniauth defaults to an :access_type of offline.
That's all it takes. With this OmniAuth strategy in place, when a domain administrator installs your application SSO will be available across the domain.

OAuth 2.0 Service Accounts

To support Yesware's reminder functionality, we needed offline access to a user's email account. In the past, this functionality was supported through 2-legged OAuth. In the new Google Apps Marketplace paradigm, OAuth 2.0 Service Accounts are the replacement.
  • In the Cloud Console, generate a private key for the OAuth 2.0 Service Account associated with your Google Apps Marketplace project. 
  • Download the .p12 private key generated. 
  • Place this key somewhere that will be accessible by your production servers, e.g., a certificates folder in your codebase. 
We used the Google API Ruby Client gem to generate an access token from our Service Account's keys.
Using the deprecated 2-Legged OAuth based approach, our authorization logic looked like this:

Gmail.connect!(:xoauth, '[email protected]', {
token: authentication.token,
secret: authentication.secret,
consumer_key: google.key,
consumer_secret: google.secret,
read_only: true
})
Using the new Service Account Based Approach, it was as follows:
key = Google::APIClient::PKCS12.load_key(
google_apps.service.p12path, # this is a constant value Google uses
# to password protect the key.
'notasecret'
)service_account = Google::APIClient::JWTAsserter.new(
google_apps.service.email,
'https://mail.google.com/',
key
)client = Google::APIClient.new(
:application_name => APPLICATION_NAME,
:version => APPLICATION_VERSION
).tap do |client|
client.authorization = service_account.authorize('[email protected]')end
Google.connect!(:xoauth2, '[email protected]', {
:oauth2_token => client.authorization.access_token,
})
With OAuth 2.0 Service Accounts, the underlying libraries we used to interact with Gmail remained the same. There were simply a few extra steps necessary to obtain an access token.

Contextual Gadgets and SSO

Yesware provides a Gmail Contextual Gadget, for scheduling email reminders. To facilitate this, it's necessary that the gadget interact with a user's email account. To make this a reality, we needed to implement SSO through our contextual gadget. Google provides great reading material on this topic. However, the approach outlined concentrates on the deprecated OpenID-based SSO approach. We used a slightly modified approach.
Rather than OpenID, we used OAuth 2.0 for associating the opensocial_viewer_id with a user. To do this, we needed to modify our OmniAuth strategy to store the opensocial_viewer_id during authentication:
# Subclass the GoogleOauth2 Omniauth strategy for
# Google Apps Marketplace V2 SSO.
module OmniAuth
module Strategies
class GoogleAppsMarketplace < OmniAuth::Strategies::GoogleOauth2

option :name, 'google_apps_marketplace'

def request_phase

# Store the opensocial_viewer_id in the session.
# this allows us to bind the Google Apps contextual
# gadget to a user account.
if request.params['opensocial_viewer_id']
session[:opensocial_viewer_id] = request.params['opensocial_viewer_id']
end

super
end

end
end
end
Once an opensocial_viewer_id was connected to a Yesware user, we could securely make API calls from our contextual gadget. To cut down on the ritual surrounding this, we wrote a Devise Google Apps OpenSocial Strategy for authenticating the OpenSocal signed requests.

Now Go Forth

Once we figured out all the moving parts, we were able to use mostly off the shelf mature libraries for building our Google Apps Marketplace Integration. I hope that this retrospective look at our development process helps other Rails developers hit the ground running even faster than we did.
Benjamin Coe profile

Benjamin Coe cofounded the email productivity company Attachments.me, which was acquired by Yesware, Inc., in  2013. Before starting his own company, Ben was an engineer at FreshBooks, the  world’s #1 accounting solution.
Ben’s in his element when writing scalable cloud-based infrastructure, and loves reflecting on the thought-process that goes into this. A rock-climber, amateur musician, and bagel aficionado, Ben can be found roaming the streets of San Francisco.
[email protected]https://github.com/bcoe@benjamincoe

Building an Enterprise File Server on Google Drive

Editor's note: This is a guest post by Thomas Gerber. Thomas is the CTO of Altirnao, the developer of the AODocs document management app on the Google Apps Marketplace. Thomas tells the story of his application and provides some tips for developers considering integrating with Google Apps and launching on the Marketplace. — Arun Nagarajan


Google Drive is increasingly popular in the enterprise, and many organizations would like to leverage it as a replacement for their existing on-premises file servers. Moving physical file servers to Drive provides many benefits, such as reliability, cost-effectiveness and the ability to access the files from anywhere and any device. However, the storage structure of Google Drive, where files are owned by many different users, is significantly different from the centralized organization of a file server, where everything is under the control of a small number of system administrators.

To address this problem, AODocs uses the Google Drive API to automatically transfer the ownership of files to a system account, and thus create a sort of “managed area” within Google Drive. With the Google Drive API, AODocs has complete control over the folder structure and the permissions of files owned by this system account. AODocs can be deployed in one click from the Google Apps Marketplace, which makes our application visible (and easy to try out!) for every Google Apps administrator in the world.



Companies who want to store their files on Google Drive may be concerned about losing control of their data (e.g. access to files being lost when an employee leaves the company) and controlling sharing permissions.

AODocs uses a single system account (i.e. a Google Apps account belonging to the customer’s domain, but not used by any human person) as a “proxy” to control the files. When a Google Drive files is added to an AODocs library, the ownership of the file is transferred to the AODocs system account and the file’s writersCanShare attribute is set to false, so that only AODocs is able to modify the file’s permissions afterwards.

To change the ownership of the file, we check if the system account can already access the file, and then either insert a new “owner” permission on it or use the Permissions.update method with the transferOwnership flag:

public void changeOwner(String user, String fileId, String newOwner) {
// Find what is the current permission of the new owner on the file
Permission newOwnerPermission = null;
PermissionList permissionList = RetriableTask.execute(new DrivePermissionListTask(drive.permissions().list(fileId)));
newOwnerPermission = findPermission(permissionList, newOwner);

if (newOwnerPermission == null) {
// New owner is not in the list, we need to insert it
newOwnerPermission = new Permission();
newOwnerPermission.setValue(newOwner);
newOwnerPermission.setType("user");
newOwnerPermission.setRole("owner");
Drive.Permissions.Insert insert = drive.permissions().insert(fileId, newOwnerPermission);
RetriableTask.execute(new DrivePermissionInsertTask(insert));
} else {
// New owner is already in the list, update the existing permission
newOwnerPermission.setRole("owner");
Drive.Permissions.Update update = drive.permissions().update(fileId, newOwnerPermission.getId(), newOwnerPermission);
update.setTransferOwnership(true);
RetriableTask.execute(new DrivePermissionUpdateTask(update));
}
}

Since all the files are owned by the system account, AODocs completely controls the lifecycle of the file (how they are created, in which folder they are located, who can change their permissions, who can delete them, etc). AODocs can thus provide higher-level document management features on top of Google Drive, such as configuring the retention time of deleted files, limiting external sharing to a whitelist of “trusted external domains”, or recording an audit log of file modifications.

As illustrated in the code snippet above, AODocs relies on the Google Drive API to perform all the operations on the managed files. The main challenge we had when using the Drive API was to properly handle all the error codes returned by the API calls, and make sure we make the difference between fatal errors that should not be tried again (for example, permission denied on a file) and the temporary errors that should be re-tried later (for example, “rate limit exceeded”). To handle that, we have encapsulated all our Google Drive API calls (we are using the Java client library) into a class named RetriableTask, which is responsible for handling the non-fatal errors and automatically retry the API calls with the proper exponential back-off. Here is a simplified version:

public class RetriableTask implements Callable {
[...]
private final Callable task;

[...]
@Override public T call() {
T result = null;
try {
startTime = System.currentTimeMillis();
result = task.call();
} catch (NonFatalErrorException e) {
if (numberOfTriesLeft > 0) {
// Wait some time, using exponential back-off in case of multiple attempts
Thread.sleep(getWaitTime());

// Try again
result = call();
} else {
// Too many failed attempts: now this is a fatal error
throw new RetryException();
}
} catch (FatalErrorException e) {
// This one should not be retried
Throwables.propagate(e);
}
return result;
}

AODocs is designed to work seamlessly with Google Drive, and our top priority is to leverage all the integration possibilities offered by the Google APIs. We are very excited to see that new features are added very often in the Admin SDK, the Google+ API, the Drive API that will allow AODocs to provide more options to system administrators and improve the experience for our end users.


Thomas Gerber profile

Thomas is the CTO of Altirnao. Before founding Altirnao, Thomas has led a team of senior technologists and architects on High Availability/High Performance implementations of enterprise software.