2011年7月25日月曜日

AppEngineアプリでAuthSubを使ってAppsのドメイン管理者かどうかをチェックする

AppsアカウントとOpenIDによる認証を行い、マーケットプレイスに載せるAppEngineのアプリで、あるAppsアカウントがドメイン管理者であるか否かを知りたい、という要件が出てきました。

そのようなAPIを探したのですが、どうもないようです。
(もしご存知の方はご一報を!)

そこで、ドメイン管理者にのみ利用可能なApps APIである、Reporting APIを使用することとしました。
つまり、Reporting APIを実行してみて、結果が無事返ってくればドメイン管理者、
認証エラーが返ってきたらドメイン管理者ではない、と判断します。

しかし、Reporting APIを呼び出す際には、ユーザー名とパスワードが必要です。
もちろんアプリ側でユーザー名とパスワードを入力してもらう画面を作ればいいのですが、
そのユーザー名とパスワードはGoogleアカウントのものであり、自分のアプリに入力させたくありません。
(利用者からすればフィッシングにも見えてしまう)
せっかく認証にOpenIDを使ったのですから、ユーザー名とパスワードは自分のアプリに入力させたくないのです。

そこで、AuthSubという、Google側で認証を行い、認証が通れば、トークン(セッションIDのようなもの)をこちらに返してくれるサービスがありますので、それを使うことにしました。

ちなみに、ソースコードは、マーケットプレースのOpenID利用のサンプルソースコードを元にしています。

基本的な方法は、ここの方法と同じです。
Google App Engine 上で認証が必要な Google Data Feeds を取得する方法

import os, re
from datetime import datetime
from datetime import timedelta
from urlparse import urlparse
import gdata.auth
from gdata import service
from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.webapp import util
import logging
import gdata.apps.reporting
import gdata.alt.appengine

def check_email(user):
  """Performs basic validation of the supplied email address as outlined
  in http://code.google.com/googleapps/marketplace/best_practices.html
  """
  domain = urlparse(user.federated_identity()).hostname
  m = re.search('.*@' + domain, user.email())
  if m:
    return True
  else:
    return False

class MainHandler(webapp.RequestHandler):
  def get(self):
    #まず最初にここが呼ばれます。
    #ログインを確認した後、AuthSubの認証をするためのリンクをユーザーに返します。
    #(ユーザーにクリックしてもらう)
    #ユーザーがクリックすると、Googleが、認証を許可するかどうかのページを表示します。
    #ユーザーが認証を許可すると、gdata.auth.GenerateAuthSubUrlのnextに指定したurlに返ってきます。
    #その際、認証トークンを、getパラメータに含めてくれます。
    template_values = {}
    user = users.get_current_user()
    if user and check_email(user):
      next = 'https://***.appspot.com/check_domain_admin'
      #scopeは、許可対象のAPIを指定します。これはReporting APIです。
      scope = ('https://www.google.com'
                    '/hosted/services/v1.0/reports/ReportingData')
      secure = 0
      session = 0 #1回限りのアクセスの場合は0(=False)。1(=True)にした場合は、トークンをセッションIDにアップグレードできます。
      auth_url = gdata.auth.GenerateAuthSubUrl(next, scope, secure=secure, session=session)
      self.response.out.write("""<html><body>
        <a href="%s">Request token for the Google Documents Scope</a>
        </body></html>""" % auth_url)     
    else:
        greeting = 'You need to log in!'
        template_values['greeting'] = greeting
        path = os.path.join(os.path.dirname(__file__), 'templates/index.html')
        self.response.out.write(template.render(path, template_values))

class CheckDomainAdmin(webapp.RequestHandler):
    def get(self):
        #認証を許可すると、ここが呼ばれます。(GenerateAuthSubUrlのnext)
        user = users.get_current_user()
        if user and check_email(user):
            self.response.out.write(user.email())
        else:
            logging.info("user NOT exist")
        #トークンを取得します。これが欲しかったんです。
        token = self.request.get("token")
        #GDataServiceの、run_on_appengineを実行する必要があります。
        #これは、「GAEのurlfetchを使え」という指示だそうです。
        srv = service.GDataService()
        gdata.alt.appengine.run_on_appengine(srv)
        #Reporting APIのpython-clientライブラリのReportRequestクラスを使います。
        req = gdata.apps.reporting.ReportRequest()
        req.token = token
        req.domain = urlparse(user.federated_identity()).hostname
        prev_day = datetime.now() - timedelta(days=1) #当日だと、まだレポートがないよ、って怒られる場合があるので一日前
        req.date = prev_day.strftime('%Y-%m-%d')
        req.report_name = "summary" #なんでもいいのですが、summaryで

        #Query url
        query = service.Query()
        query.feed = ('https://www.google.com'
                    '/hosted/services/v1.0/reports/ReportingData')
        try:
            #converterは、デフォルトのGDataEntryFromStringがエラーになります。
            feed = srv.Post(req.ToXml(), query.ToUri() ,converter=str) 
            self.response.out.write(feed) #これは別にいらないです。確認してるだけ
            #Exceptionが発生しなければDomain Adminと判断します。
        except gdata.service.RequestError,e:
            #Domain Adminでは無い場合、RequestErrorが発生します。
            #このとき、POSTの戻り値は、<hs:reason>AuthenticationFailure(1006)</hs:reason>を含んでいます。
            logging.info(e)

class OpenIDHandler(webapp.RequestHandler):
    def get(self):
      """Begins the OpenID flow and begins Google Apps discovery for the supplied domain."""
      self.redirect(users.create_login_url(dest_url='http://***.appspot.com/',
                                           _auth_domain=None,
                                           federated_identity=self.request.get('domain')))

def main():
  application = webapp.WSGIApplication([('/', MainHandler),
                                        ('/check_domain_admin', CheckDomainAdmin),
                                        ('/_ah/login_required', OpenIDHandler)],
                                       debug=True)
  util.run_wsgi_app(application)

if __name__ == '__main__':
  main()
当然ですが、「http://***.appspot.com」は自分のアプリに合わせてくださいね
Reporting APIのpython クライアントライブラリは、↓ここにあります。
google-apps-reporting-api-client

ソースコードではgdata.apps.reportingとしてインポートしているやつです。
最初はこのライブラリのReportRunnerクラスを使おうと思ったのですが、上手くいきませんでした。
ReportRunner.GetReportDataを実行すると、400 BadRequestが返ってきます。run_on_appengineが関係してる?
上記のソースコードでは、ReportRequestクラスを使ってるだけです。

5 件のコメント:

  1. マーケットプレイスを使っているのであれば、2LOでProvisioning APIを使うのがシンプルかと思います。

    返信削除
  2. おお!こんなところまで見て頂けるとは!初コメントありがとうございます。

    Provisioningだと、ドメイン管理で許可されていない場合に使えないかなーと思ったんです
    あれでも、2LOでReportingAPIを使わなかった理由はなんだっけ・・
    もう一度調べてみます。この「ダサイ」方法は嫌なんですよね

    返信削除
  3. マーケットプレイスが前提の時点で、manifestのscopeに https://apps-apis.google.com/a/feeds/user/#readonly を含めておけば、インストールした=Provisioning APIの使用も承認した、という事になります。そんなワケでマーケットプレイス2LOはとても便利です。

    返信削除
  4. そんなことができるんですか!ありがとうございます!

    返信削除
  5. できました!あっさりとwありがとうございます!

    返信削除