Working with the Google Directory API Ruby client

27 Apr 2015

Recently I had to transition a Rails app from Google's old provisioning API to the new-ish Directory API. There were a few bits that I had trouble with, and some of the Stack Overflow threads reflected similar confusion, so here's a writeup in hopes of helping others.

The Directory API is a pretty standard REST setup, so you could just hit it with httparty or faraday or some such HTTP client. But I think most people will want to use the google-api-client gem since that's what Google provides. This gem uses an interesting technique; it doesn't define a bunch of methods like Group#update and whatnot. Instead it downloads an API definition file and at runtime uses that to generate classes and methods. This means that Google doesn't have to release a new gem version every time they add an API endpoint, which is pretty cool. But it does mean that you can't just bundle open the gem and see what the method names are.

You don't want to download that API file every time you hit the API, so you'll want to do something to cache it locally. I created a GoogleAdminDirectoryWrapper class in lib/ for easier testing and command line usage; here's the part of that class that caches the API definition file:

  def dump_directory_to_file
    File.open(discovered_directory_api_file, "w") do |f|
      f.syswrite(JSON.dump(client.discovery_document("admin", "directory_v1")))
    end
  end
  def discovered_directory_api_file
    "#{Rails.root}/config/discovered_directory_api.json"
  end

Then you can jump in a console, run GoogleAdminDirectoryWrapper.new.dump_directory_to_file, and you've got the file locally. In order to use the API definition you need to register it with an instance of Google::APIClient, so you'll want something like:

  def directory_api_from_file
    return @directory_api if @directory_api
    contents = File.read(discovered_directory_api_file)
    client.register_discovery_document("admin", "directory_v1", contents)
    @directory_api = client.discovered_api("admin", "directory_v1")
  end

The client method is another helper I wrote that returns a properly initialized Google::APIClient, i.e., one that's been authorized with OAuth using an instance of Signet::OAuth2::Client. Getting a service (i.e., a web app) to authenticate is a whole discussion in itself. The one hint (and the documentation calls this out, but still) that I'd give is that when authorizing you need to use a :person entry in the hash where the value is the email of the administrative account that you're "impersonating", that is, the account with which you created the application that's making these calls.

So, back to API usage. When I was writing the code that calls the endpoints it wasn't always clear to me how to actually compose those method invocations. The invocations vary based on the HTTP method and the content of the request; they depend on what's in the URL itself and what's in the request body. It's a sort of mapping technique; for example, here's more or less my function for deleting a group:

  def group_delete(email)
    client.execute(directory_api_from_file.groups.delete, :groupKey => email)
  end

Seems pretty straightforward. But adding a member to a group looks a little different since it's a HTTP POST that requires information in the request body. You have to supply an api_method parameter, a parameters containing the URL parameters, and a body_object containing the POST request body:

  def group_add_member(group_email, user_email)
    p = {:groupKey => group_email}
    b = {:email => user_email, :role => "MEMBER"}
    m = directory_api_from_file.members.insert
    client.execute(:api_method => m, :parameters => p, :body_object => b)
  end

Here's another example. To fetch all members of a group you're doing a GET and so everything is contained in the URL and in the parameters. So we need api_method and parameters but no body_object. The method below also takes care of unpacking the relevant (for my purposes) bits from the data structures that the gem returns:

  def group_retrieve_all_members(group_email)
    p = {:groupKey => group_email}
    m = directory_api_from_file.members.list
    res = client.execute(:api_method => m, :parameters => p).data
    res.members.map {|m| m.email.split('@').first.downcase }.sort
  end

After doing a couple of these a pattern becomes clear and you can just bang them out. I found it really helpful to sit in a console and do load 'lib/google_admin_directory_wrapper.rb' while I experimented with the method definitions and only after I nailed it down would I write client code within my application.

I thought some of the error handling was a little surprising. For example, the API responds with a permissions error if the group does not exist, so you can do something like this to check for a group's existence:

  def group_exists?(group_email)
    p = {:groupKey => group_email}
    m = directory_api_from_file.groups.get
    !client.execute(:api_method => m, :parameters => p).error?
  end

For testing, I used mocha to mock out method invocations on GoogleAdminDirectoryWrapper and supply the expected results. I feel a little bad about not mocking things at a lower level - i.e., using vcr. I didn't actually change anything, but I do feel bad, so that's got to count for something.

I think the lessons learned are pretty much the usual ones. Get started on API migrations early (which I didn't). Do yourself a favor and put together a rapid development cycle. Get simple things working and build from there. RTFM.