2013-01-12
Heroku上からService Account認証でGoogle APIにアクセスする
Googleの様々なAPIを叩く際、認証にOAuth 2.0を用いる。
使用する場面やパターンによって以下のような6つのシナリオが想定されている。
- Login
- Web Server Applications
- Client-side Applications
- Installed Applications
- Devices
- Service Accounts
多くの場合は「ユーザごとに認証させて個別のtokenを発行しリクエストに利用する」という流れなのだけど、中にはたとえばURL短縮APIとか、必ずしもユーザ個別にtokenを発行させる必要がないこともある。
そういう場合には「サービス固有のtoken」だけあれば良い。ということで使えるのが「Service Accounts」という方式。
サービスアカウントに発行されたprivate keyを使って署名したJWTリクエストを生成してAPIを叩く、という仕組みのようだ。
rubyでgoogle-api-clientを使う例
GoogleのAPIを叩くためのライブラリとして、Ruby Gemsではgoogle-api-clientというのがある。
google-api-client | RubyGems.org | your community gem host
JWTを使ったリクエストや認証なども対応してくれている。
これを使って実際にやってみる。
準備
にて、Application typeを"Service account"と選択してClient IDを生成する。
すると、"Email address"が発行され、private keyがダウンロードできるようになるので、これを使う。
書く
Gemfileに
source :rubygems gem 'google-api-client', '0.6.0'
と書いてbundle installして、以下のようなスクリプトを書く。
require 'google/api_client' client = Google::APIClient.new(:application_name => '') key = Google::APIClient::PKCS12.load_key('/Users/sugyan/Downloads/...-privatekey.p12', 'notasecret') client.authorization = Signet::OAuth2::Client.new( :token_credential_uri => 'https://accounts.google.com/o/oauth2/token', :audience => 'https://accounts.google.com/o/oauth2/token', :scope => 'https://www.googleapis.com/auth/urlshortener', :issuer => <発行されたemail address> :signing_key => key, ) client.authorization.fetch_access_token! shortener = client.discovered_api('urlshortener') result = client.execute( :api_method => shortener.url.insert, :body_object => { :longUrl => 'http://d.hatena.ne.jp/sugyan/' }, ) puts result.data.id
ダウンロードしたprivate keyのファイルを読みこんでkeyを生成し、email addressやscopeを指定し、認証。
これで、GoogleのURL短縮APIを叩いて短縮URLを得ることができる。
$ bundle exec ruby shorten.rb http://goo.gl/zLVjD
Herokuに上げるために
で、こんなものをHeroku上で動かそうとすると、API Consoleからダウンロードしたprivate keyのファイルをgit repositoryに含める必要が出てしまう。それはイヤだ。
幸いにも、Google::APIClient::PKCS12でloadしたkeyは文字列として得られる。
$ bundle exec ruby -r 'google/api_client' -e 'puts Google::APIClient::PKCS12.load_key("/Users/sugyan/Downloads/...-privatekey.p12", "notasecret")'
-----BEGIN RSA PRIVATE KEY-----
MIICXQ...
...
-----END RSA PRIVATE KEY-----
なので、これを丸ごとheroku configで渡してしまえばいい。ついでに発行されたemail addressも。
$ heroku config:set GOOGLE_API_KEY="$(bundle exec ruby -r 'google/api_client' -e 'puts Google::APIClient::PKCS12.load_key("/Users/sugyan/Downloads/...-privatekey.p12", "notasecret")')"
$ heroku config:set GOOGLE_API_EMAILADDRESS=...@developer.gserviceaccount.com
で、これを使用してKeyを生成するようにコードを変更する。
require 'google/api_client' client = Google::APIClient.new(:application_name => '') key = OpenSSL::PKey::RSA.new(ENV['GOOGLE_API_KEY']) client.authorization = Signet::OAuth2::Client.new( :token_credential_uri => 'https://accounts.google.com/o/oauth2/token', :audience => 'https://accounts.google.com/o/oauth2/token', :scope => 'https://www.googleapis.com/auth/urlshortener', :issuer => ENV['GOOGLE_API_EMAILADDRESS'], :signing_key => key, ) client.authorization.fetch_access_token! shortener = client.discovered_api('urlshortener') result = client.execute( :api_method => shortener.url.insert, :body_object => { :longUrl => 'http://d.hatena.ne.jp/sugyan/' }, ) puts result.data.id
これで、このへんをcommitしてherokuにpushすると、設定したconfigの文字列を使用してheroku上からService Accounts認証を使ってAPIを叩けるようになる。
$ git push heroku master
Counting objects: 6, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 1.10 KiB, done.
Total 6 (delta 0), reused 0 (delta 0)
-----> Ruby app detected
-----> Installing dependencies using Bundler version 1.3.0.pre.5
Running: bundle install --without development:test --path vendor/bundle --binstubs vendor/bundle/bin --deployment
Fetching gem metadata from http://rubygems.org/.........
Fetching gem metadata from http://rubygems.org/..
Installing addressable (2.3.2)
Installing extlib (0.9.16)
Installing multi_json (1.5.0)
Installing autoparse (0.3.2)
Installing multipart-post (1.1.5)
Installing faraday (0.8.4)
Installing jwt (0.1.5)
Installing launchy (2.1.2)
Installing signet (0.4.4)
Installing uuidtools (2.1.3)
Installing google-api-client (0.6.0)
Using bundler (1.3.0.pre.5)
Your bundle is complete! It was installed into ./vendor/bundle
Cleaning up the bundler cache.
...
* [new branch] master -> master
$ heroku run bundle exec ruby shorten.rb
http://goo.gl/WnqP5
まとめ
…と、そういうようなことが、以下の記事に書いてあった。
2012-08-14
数十行の簡単なスクリプトでWebコンテンツの更新を検知し通知する
以前にも似たようなのは書いたのだけど。
某アイドルグループに、ついに待望のファンクラブが作られた。もちろんすぐに入会しました。
そこではその子たちのマネージャーさんが不定期に日記を更新してくれるのだけど、残念なことにRSSとかも無いし、自分でログインして覗いてみないと更新されているかどうかを知ることができない。
自動化せずにはいられない。
ということで、更新チェックするプログラムを書くのだけど、、、
コンテンツが「更新されたか否か」を判定するためには、「一定間隔でコンテンツの内容をチェック」し、「前回チェックしたときと比較して差異があるか否か」を調べる必要がある。
「一定間隔で内容をチェック」すること自体は、cronなどを使えば簡単にできる(ここではWWW::Mechanizeでログイン、Web::ScraperでコンテンツHTMLの解析を行う)。
けど、「前回チェックしたときと比較して差異があるか否か」を調べるためには、前回チェックしたときの結果を何らかの形で残しておく必要がある。ファイルに書き出すだとか、DBに保存するだとかの形で。
そうするのは面倒なので、そもそもプログラムを終了させる形で書かずに「一定間隔で内容チェック」だけを行うよう実行させ続け、チェックした結果はメモリに残しておくようにする。
AnyEventのtimerで一定間隔ごとにMechanizeを走らせるようにする。
#!/usr/bin/env perl use strict; use warnings; use utf8; use AnyEvent; use Config::Pit; use Encode 'encode_utf8'; use Log::Minimal; use Try::Tiny; use Web::Scraper; use WWW::Mechanize; my $conf = +{ angeleyes => pit_get('fc.momoclo.net', require => { login_id => '会員番号', password => 'パスワード', }), }; my $previous = undef; my $cv = AE::cv; my $w = AE::timer 0, 100, sub { try { my $mech = WWW::Mechanize->new; $mech->get('https://fc.momoclo.net/pc/login.php'); $mech->submit_form( form_id => 'loginForm', fields => $conf->{angeleyes}, ); my $latest = scraper { process '#topdiary .cont li', 'contents[]' => scraper { process 'a', 'title' => 'TEXT'; process 'p.date', 'date' => 'TEXT'; }; }->scrape($mech->content)->{contents}[0]; $latest->{date} =~ s/更新//; my $current = sprintf '(%s) %s', $latest->{date}, encode_utf8 $latest->{title}; infof('latest: %s', $current); if (defined $previous && $previous ne $current) { # 何らかの通知を行う } $previous = $current; } catch { warnf('error: %s', $_); }; }; $cv->recv;
https://gist.github.com/516f6d3f7297d2865a93
プログラムはAnyEvent::timerによって回り続けるので終了せず、定期的にコンテンツをチェックした内容は$previous変数に保持され続け、更新されたときだけ通知処理を行う、ということがこの単一のスクリプトだけで実現できる。cronを使う必要は無い。
こういうのはNode.jsとかでsetIntervalを使っても一緒だと思うけど、Mechanize的なモジュールをよく知らないのでPerlで書いた。
WWW::Mechanize的なのってどういうのがあるんだろう?
ももクロ春の一大事2012~横浜アリーナ まさかの2DAYS~ BD-BOX【初回限定盤】 [Blu-ray]
- 出版社/メーカー: キングレコード
- 発売日: 2012/09/05
- メディア: Blu-ray
- 購入: 10人 クリック: 113回
- この商品を含むブログ (27件) を見る
- アーティスト: ももいろクローバーZ
- 出版社/メーカー: キングレコード
- 発売日: 2012/06/27
- メディア: CD
- 購入: 4人 クリック: 433回
- この商品を含むブログ (50件) を見る
追記
少なくともPerlの場合はwhile文でループさせつつsleepで、という方法でも良かったはず。Nodeで最初にsetIntervalで書こうとしていてこういう形になってしまったのかも…
2012-07-03
Test::Flattenでsubtestの初っ端で死ぬときのエラーが出力されない (追記あり)
@xaicronさんのTest::Flattenを使ってみている。
use Test::More; use Test::Flatten; subtest 'hoge' => sub { ok 1, 'fuga'; piyo(); }; done_testing;
という、subtest内で死んでしまうようなテストの場合、proveを実行すると
$ prove -v t/01_hoge.t .. # ------------------------------------------------------------------------------ # hoge # ------------------------------------------------------------------------------ ok 1 - fuga Undefined subroutine &main::piyo called at t/01_hoge.t line 6. # Tests were run but no plan was declared and done_testing() was not seen. Dubious, test returned 255 (wstat 65280, 0xff00) All 1 subtests passed Test Summary Report ------------------- t/01_hoge.t (Wstat: 65280 Tests: 1 Failed: 0) Non-zero exit status: 255 Parse errors: No plan found in TAP output Files=1, Tests=1, 0 wallclock secs ( 0.02 usr 0.00 sys + 0.01 cusr 0.00 csys = 0.03 CPU) Result: FAIL
とエラーを吐いて失敗を教えてくれるけど、
use Test::More; use Test::Flatten; subtest 'hoge' => sub { piyo(); ok 1, 'fuga'; }; done_testing;
と、subtest内の最初で死ぬようなパターンだと、
$ prove -v t/01_hoge.t .. # ------------------------------------------------------------------------------ # hoge # ------------------------------------------------------------------------------ No tests run for subtest hoge at t/01_hoge.t line 7. Dubious, test returned 255 (wstat 65280, 0xff00) No subtests run Test Summary Report ------------------- t/01_hoge.t (Wstat: 65280 Tests: 0 Failed: 0) Non-zero exit status: 255 Parse errors: No plan found in TAP output Files=1, Tests=0, 0 wallclock secs ( 0.02 usr 0.00 sys + 0.01 cusr 0.00 csys = 0.03 CPU) Result: FAIL
となり、"No tests run for subtest $caption"がcroakされてしまい、何故死んでしまったのかが分からない。
どうすればいいんだろう
追記
上記記事を書いてみたところ、xaicronさんがあっという間に直してくれました。
@sugyan shipped 0.09!
2012-07-03 18:18:02 via Echofon to @sugyan
0.09であれば上記の問題は起こらないようです。ありがとうございます!
2012-03-22
Crypt::SSLeayがLANGの関係でインストールできないとき
あるLinux環境でCrypt::SSLeayをインストールしようとするとテストが通らない。
$ cpanm -v Crypt::SSLeay ... t/00-basic.t .... ok t/01-connect.t .. 1/8 # Failed test 'Net::SSL->new' # at t/01-connect.t line 28. # Connect failed: connect: 接続を拒否されました; 接続を拒否されました at t/01-connect.t line 11 # Looks like you failed 1 test of 8. t/01-connect.t .. Dubious, test returned 1 (wstat 256, 0x100) Failed 1/8 subtests (less 7 skipped subtests: 0 okay) t/02-live.t ..... 1/4 # config on linux # ssl OpenSSL 1.0.0-beta3 in /usr; # lib -L -lssl -lcrypto -lgcc # inc -I/usr/include # cc cc t/02-live.t ..... ok Test Summary Report ------------------- t/01-connect.t (Wstat: 256 Tests: 8 Failed: 1) Failed test: 1 Non-zero exit status: 1 Files=3, Tests=24, 0 wallclock secs ( 0.05 usr 0.01 sys + 0.22 cusr 0.03 csys = 0.31 CPU) Result: FAIL Failed 1/3 test programs. 1/24 subtests failed. make: *** [test_dynamic] エラー 255 FAIL ! Installing Crypt::SSLeay failed. See /home/sugyan/.cpanm/build.log for details.
どうやらNet::SSL->newが失敗したときのエラーメッセージが日本語で出てきてしまっているかららしい。テスト内部では英語のエラーメッセージが期待されているためコケてしまうようだ。
- Perl ローカルPCにCrypt::SSLeay(Net::SSL)を入れるときの注意点 | smokycat.info
- Hinemos 研究日記 » Blog Archive » 【Perl】Crypt::SSLeayをインストールする際に気を付けること
ということで
$ LANG=C cpanm -v Crypt::SSLeay
とすればインストールできるようになるらしい。
このエラーメッセージの言語ってどこで決まるんだろう?手元のMac OS X環境ではLANG=ja_JP.UTF-8でも問題ないのだけど…。
2012-03-20
gitoliteでプライベートリポジトリ作成
月額980円のさくらVPSを個人用に使い倒す - ゆーすけべー日記を読んで、そういえばgitのprivate repositoryを持ってないし何かのときのために設定しておくか、と思ってさくらVPSのサーバにgitoliteを入れてみた。
以下、手順メモ。
gitoliteのインストール
yumでサクっと。
sugyan@remote $ sudo yum install gitolite
gitユーザを作成
専用ユーザを作ってそいつの$HOME以下でリポジトリ管理するようにした方がなにかとラクなので
sugyan@remote $ sudo useradd -m git
adminになるため公開鍵をコピー
sugyan@remote $ sudo cp $HOME/.ssh/id_dsa.pub /home/git/admin.pub
setup
コピーしてきた公開鍵を使ってsetup。使うコマンドはgl-setupだけ
sugyan@remote $ su - git git@remote $ gl-setup -q admin.pub creating gitolite-admin... Initialized empty Git repository in /home/git/repositories/gitolite-admin.git/ creating testing... Initialized empty Git repository in /home/git/repositories/testing.git/ [master (root-commit) 9a57094] start 2 files changed, 6 insertions(+), 0 deletions(-) create mode 100644 conf/gitolite.conf create mode 100644 keydir/admin.pub git@remote $ ls repositories/ gitolite-admin.git testing.git
repositoryが出来上がってる。
gitolite-adminをclone
adminとしてgitolite-adminをcloneしてくる。同一サーバ内から持ってくるのでhost名はlocalhost指定で良い
sugyan@remote $ mkdir ~/temp sugyan@remote $ cd ~/temp sugyan@remote $ git clone git@localhost:gitolite-admin Initialized empty Git repository in /home/sugyan/temp/gitolite-admin/.git/ remote: Counting objects: 6, done. remote: Compressing objects: 100% (4/4), done. remote: Total 6 (delta 0), reused 0 (delta 0) Receiving objects: 100% (6/6), done. sugyan@remote $ ls gitolite-admin
cloneできた。
ローカルからも使えるようにする
ローカル環境の公開鍵を持ってきて登録する。
sugyan@local $ scp $HOME/.ssh/id_dsa.pub vps:temp/gitolite-admin/keydir/sugyan-local.pub
再びsshでvpsサーバに入ってgitolite-admin上で公開鍵をaddしてpush
sugyan@local $ ssh vps sugyan@remote $ cd ~/temp/gitolite-admin sugyan@remote $ git status # On branch master # Untracked files: # (use "git add <file>..." to include in what will be committed) # # keydir/sugyan-local.pub nothing added to commit but untracked files present (use "git add" to track) sugyan@remote $ git add keydir/sugyan-local.pub sugyan@remote $ git commit -m 'add sugyan-local' [master a193304] add sugyan-local 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 keydir/sugyan-local.pub sugyan@remote $ git push Counting objects: 6, done. Delta compression using up to 2 threads. Compressing objects: 100% (4/4), done. Writing objects: 100% (4/4), 842 bytes, done. Total 4 (delta 0), reused 0 (delta 0) remote: remote: ***** WARNING ***** remote: the following users (pubkey files in parens) do not appear in the config file: remote: sugyan-local(sugyan-local.pub) To git@localhost:gitolite-admin 9a57094..a193304 master -> master
confに何も書き足していないので警告は出るけど、まぁ今は問題無し。
ローカルから確認。gitolite-adminは権限ないからcloneできないけど、デフォルトでtestingは@allに対してRW+になっているのでcloneできるしpushもできる。
sugyan@local $ git clone git@vps:gitolite-admin Cloning into gitolite-admin... R access for gitolite-admin DENIED to sugyan-local (Or there may be no repository at the given path. Did you spell it correctly?) fatal: The remote end hung up unexpectedly sugyan@local $ git clone git@vps:testing Cloning into testing... warning: You appear to have cloned an empty repository. sugyan@local $ cd testing sugyan@local $ echo hoge > hoge sugyan@local $ git add hoge sugyan@local $ git commit -m 'add hoge' [master (root-commit) 3285627] add hoge 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 hoge sugyan@local $ git push origin master Counting objects: 3, done. Writing objects: 100% (3/3), 205 bytes, done. Total 3 (delta 0), reused 0 (delta 0) To git@vps:testing * [new branch] master -> master
大丈夫げ。
グループとリポジトリを作る
gitolite-adminにてconf/gitolite.confを編集。@sugyanというグループを作り、そこにsugyan-localを加える
sugyan@remote $ emacs conf/gitolite.conf
sugyan@remote $ git diff
diff --git a/conf/gitolite.conf b/conf/gitolite.conf
index 2d55bf8..202384a 100644
--- a/conf/gitolite.conf
+++ b/conf/gitolite.conf
@@ -1,5 +1,10 @@
+@sugyan = sugyan-local
+
repo gitolite-admin
RW+ = admin
repo testing
RW+ = @all
+
+repo my-private
+ RW+ = @sugyan
sugyan@remote $ git add conf/gitolite.conf
sugyan@remote $ git commit -m 'add new group and repository'
[master e3c35ed] add new group and repository
1 files changed, 5 insertions(+), 0 deletions(-)
sugyan@remote $ git push
Counting objects: 7, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (4/4), 409 bytes, done.
Total 4 (delta 0), reused 0 (delta 0)
remote: creating my-private...
remote: Initialized empty Git repository in /home/git/repositories/my-private.git/
To git@localhost:gitolite-admin
a193304..e3c35ed master -> master
my-privateレポジトリが新しく作成された。
ローカルから確認。
sugyan@local $ git clone git@vps:my-private Cloning into my-private... warning: You appear to have cloned an empty repository. sugyan@local $ cd my-private sugyan@local $ echo fuga > fuga sugyan@local $ git add fuga sugyan@local $ git commit -m 'add fuga' [master (root-commit) 5955bdb] add fuga 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 fuga sugyan@local $ git push origin master Counting objects: 3, done. Writing objects: 100% (3/3), 207 bytes, done. Total 3 (delta 0), reused 0 (delta 0) To git@vps:my-private * [new branch] master -> master
testing同様に読み書きできた。
別のマシンからも使いたい場合は公開鍵を登録して@sugyanグループに追加してやれば良いし、他の人と共同で使うことになれば都度グループなり権限なりを追加していけば良い。
感想
最初なんだかよく分からずちょっと詰まったけど、一度設定できてしまえば結構ラクに管理できて良さそう。もっと細かい権限設定なども出来るらしい。必要になったら調べて設定してみよう


