2011年7月31日日曜日

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

shin1ogawaさんからご指摘頂いた方法です。
すげえ簡単じゃん・・AuthSubとかアホすぎる・・
この投稿は、AppEngineアプリでAuthSubを使ってAppsのドメイン管理者かどうかをチェックする の続きです。

まず、マーケットプレースに登録するアプリの、Application Manifestで、
Provisioning APIを使うよ、って宣言しときます。
<?xml version="1.0" encoding="UTF-8" ?>
<ApplicationManifest xmlns="http://schemas.google.com/ApplicationManifest/2009">
  <Name>Hello World</Name>
  <Description>Demonstrates a simple Google Apps Marketplace application</Description>

  <!-- Administrators and users will be sent to this URL for application support -->
  <Support>
    <Link rel="support" href="https://***.appspot.com/" />
  </Support>

  <!-- Show this link in Google's universal navigation for all users -->
  <Extension id="navLink" type="link">
    <Name>Hello World</Name>
    <Url>https://***.appspot.com?from=google&amp;domain=${DOMAIN_NAME}</Url>
    <Scope ref="ProvisioningAPI"/>
  </Extension>

  <!-- Declare our OpenID realm so our app is white listed -->
  <Extension id="realm" type="openIdRealm">
    <Url>https://***.appspot.com</Url>
  </Extension>
  <!-- Need access to the Provisioning API -->
  <Scope id="ProvisioningAPI">
    <Url> https://apps-apis.google.com/a/feeds/user/#readonly</Url>
    <Reason>This app grants some control to domain admin.</Reason>
  </Scope>
</ApplicationManifest>

上の様にScopeを定義しておくと、「Add it Now」でのインストール時に、こんな感じで聞いてきます。












「データアクセスを許可する」をクリックすることで、Provisioning APIの使用が可能になります。
なお、この許可は、ドメイン管理画面から後で取り消すこともできますし、
ここでキャンセルしたとしても、ドメイン管理画面から後で許可することもできます。

んで、アプリの方はこんな感じ。一部抜粋です。

from gdata.apps.service import AppsService 

CONSUMER_KEY = "..."
CONSUMER_SECRET = "..."
SIG_METHOD = gdata.auth.OAuthSignatureMethod.HMAC_SHA1

class MainHandler(webapp.RequestHandler):
  def get(self):
    template_values = {}
    user = users.get_current_user()
    if user and check_email(user):
        srv = AppsService(domain=urlparse(user.federated_identity()).hostname , source="Hello World")
        srv.SetOAuthInputParameters(SIG_METHOD, CONSUMER_KEY, consumer_secret=CONSUMER_SECRET,
                                   two_legged_oauth=True, requestor_id=user.email())
        srv.ssl = True
        gdata.alt.appengine.run_on_appengine(srv)
        user_name = user.email().split('@', 1)[0]
        user_entry = srv.RetrieveUser(user_name)
        if user_entry.login.admin == "true":
            self.response.out.write("%s is admin." % user_name)
        else:
            self.response.out.write("%s is not admin." % user_name)
    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))
CONSUMER_KEY とCONSUMER_SECRETは、マーケットプレースの「My Vendor Profile」を開いて、
リスティングの「View OAuth Consumer Key 」で見ることができます。

ホント、shin1ogawaさんに感謝です!ありがとうございます!

2011年7月25日月曜日

MarketplaceでのGoogleへの支払いについて

元ネタ:Payment Policy

・登録料

マーケット出品者のアプリをマーケットプレースに掲載するための料金。100US$。
Googleに対し、1回だけ支払う。(ベンダーごとに1回?アプリごとに1回?不明)

・インストール時の料金

「Add it Now」を使用して、ユーザーがアプリをインストールした際に発生する料金のうち、
20%をGoogleに支払う。

・その他

マーケット出品者が、顧客に請求する料金のうち、以下のものは、Googleへの支払いの対象となる。
  1. Google Appsにインストールされるアプリに料金が発生した場合(例えば、インストール時は無料で、無料試用期間の終了後に料金が発生するようなアプリの料金を指します。)
  2. 定期的に発生する基本使用料
  3. 従量課金される料金(例えば、データ量に応じてとか、使用時間に応じて、とか)
  4. 製品エディションのアップグレード時に発生する料金。例えば
スタンダード版をプレミアム版にアップグレードする場合の料金
ユーザー数(ライセンス数)の追加により発生する料金
ストレージ容量の追加などのアップグレード

また、マーケット出品者が、顧客に請求する料金のうち、以下のものは、Googleへの支払いの対象とならない。

  1. 既存の顧客の既存の支払い
  2. 広告料
  3. 導入費用、コンサル費用、サポート費用などのサービス料
  4. Ecommerceによる、商品販売の料金(チケットや、サービスの予約、書籍、食品などの販売)
  5. もはやGoogle Appsユーザーではなくなった顧客からの支払い
  6. 税金
  7. 支払い免除期間に獲得した顧客からの支払い(例えば、以下に示す、『Revenue sharing exemption period』

・無料で使えるものは?

Googleの無料API、ガジェット、OpenIDサービスなどは、無料で使えます。
また、「Add it Now」フィーチャーを使わない、インストールできないアプリをリストすることができます。

ただし、「Add it Now」フィーチャーを使用してインストールするアプリのみが、
Googleユニバーサルナビゲーションリンク、Appsコントロールパネル、または
その他Googleがインストールアプリのロードマップに載せている機能にアクセスすることができます。

・質問があったら?

もし、あなたのアプリの課金方法が、上記の例に当てはまらないのであれば、
あなたのアプリと課金方法の詳細を「solutions-marketplace-feedback@google.com」にメールしてください。
課金対象となるか否かをこちらで判断してお返事します。
もし、こちらの判断に同意いただけない場合は、アプリをインストールできない状態のままにしておくか、リストから削除してください。

・インストール可能なアプリの販売について

Google Appsマーケットプレースを通じて、インストール可能なアプリを販売する場合は、Googleへの支払いが必要です。
Googleへの支払いはGoogle Checkoutによって行われます。
すべての販売、アップグレード、その他関連する契約は、GoogleCheckoutを通さなければなりません。
それらの請求をシステムに統合する詳細な方法については、後で説明します。(どこ?)

・Google Apps マーケットプレースでの支払いについて
支払い免除期間(Revenue sharing exemption period)


Google Apps マーケットプレースは、現在のところ、請求機能をサポートしていません。
そのため、2010年3月9日以降、【あなたの所在地でのマーケットプレースBilling APIのリリース3ヶ月後】までの間、Googleへの支払いを免除します。
なお、この免除は予告無しに取り消される可能性があります。
また、Checkout APIの使用に関して、「開発者の所在地がどこであるか」は、Googleが決定します。

Googleは、開発者に向けて、APIとテスト環境を早いうちにリリースするつもりです。
開発規約に規定されているように、開発者の所在地でのBilling APIのリリース後3ヶ月以内に、開発者は、BillingAPIをアプリに統合しなければなりません。
Googleは、Google Apps Developer Blogや、emailを通じて、APIがリリースされ、支払い免除期間が終了したことを通知します。
Googleは、2010年の第4四半期にBilling APIをリリースする予定です。(まだ?今日は2011/07/26)

支払い免除期間中に獲得した顧客については、支払い免除期間の終了後も支払いをする必要はありません。
より詳しい情報は、Payment Policy FAQを見てください。

・Google AppsマーケットプレースBillingの統合について(?)

Google Checkoutに基づくGoogle Appsマーケットプレース支払いサービスは、マーケットプレースのベンダーへの支払いも扱います。
開発者は、アプリのマーケットプレースへのリスティングの過程で、アプリの値段や更新料を決定できるようになります。

・Checkout API --まもなく公開--

APIのドキュメントは、code.google.comでまもなく公開されます。

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クラスを使ってるだけです。

2011年7月24日日曜日

アプリケーションのリスティング

元ネタ:Writing your First Marketplace App using Python


英語のドキュメントでは、アプリケーションのマーケットプレイスへの登録を「Listing」と表現しているため、「リスティング」と書くことにします。

リスティングするリストには、「プライベートリスト」というものがあり、
これは開発とテストのために利用することができます。

マーケットプレイスのページを開いて、まずは「Become a Vendor」にてベンダーとして登録する。
これは、どんなGoogleアカウントでも可能です。
(Gmailアカウントでも、Appsアカウントでも)

でも、一人でたくさんGoogleアカウントを持っているからと言って、
アカウント間で、アプリケーションリストを共有することはできません。

ベンダー登録の後、リスティングを行うわけですが、
この際、 "My product may be directly installed into Google Apps domains"をチェックします。
アプリケーションのカテゴリと同様、これは、後で変更することができません。
また、現在のところ、リストから削除することもできません。

元ネタページのmanifest.xml をコピーして、「ApplicationManifest」欄に貼り付け、
「https://my-app.appspot.com」を、自分のアプリに合わせて変更します。
「ListingManifest」は、テストなのでとりあえず空欄でいいです。

「Save and Preview」ボタンを押せばリスティングは完了です!




※リスティング後、「Please review (and test) your listing.」と表示されます。
これがプライベートリストの状態で、他の人からは見えないが、テストできる状態、
ということになります。

テスト完了後、「I am ready! Submit this listing for approval」をクリックすることで、
グーグルの承認プロセスに回され、承認後にアプリケーションリストに出現することになります。
初回リスティング時に、100$がグーグルから請求されます、と書いてあります。
ということは、2回目以降は請求されないのでしょうか・・ベンダーあたり1回だけ?なのかな?