Cloud FunctionsでIAMを利用する

Firebase便利ですよね。

とくに最近はCloud Functionsで簡単なServerlessプログラムを書いてライフハック*1に利用することが多いです。

Cloud FirestoreのイベントトリガーだけでなくHTTPリクエストによるトリガーも可能なため、Cron Jobを使用して定期的に関数を実行したりしています。

f:id:andoshin11:20180423234039p:plain

セキュリティとアクセス制限

Cloud Functionsの長所は誰でもHTTPリクエストで起動できるところであり、短所もまたその点にあります。

これは特定の権限を持った発信元のみに実行を許可したい際、問題になるでしょう。

今回は公式でも紹介されている上記の方法でその問題を解決します。

具体的には新たにCloud Storage Bucketを作成し、承認を行うプロキシとして利用する方法です。BucketはCloud Functionsと異なり柔軟な承認の仕組みを有しているため、リクエストが実行されるごとにそちらのAPIで権限を確認してレスポンスを切り替えます。

前提条件

以下の項目を満たしていることを確認してください。

ついでに複数のGCPプロジェクトを管理している方は以下の設定でデフォルトプロジェクトを変更しておくと便利です

$ gcloud config set core/project [PROJECT_NAME]

サービスアカウントを作成する

権限を付与するアカウントをCLIから作成します

$ gcloud iam service-accounts create test-account --display-name "Test Account"
> Created service account [test-account].

GCPコンソールから「IAMと管理」 -> 「サービスアカウント」と進むとアカウントが作られたことを確認できるはずです

f:id:andoshin11:20180423205631j:plain

Bucketの作成

Cloud Storage BucketCLIから作成します

$ gsutil mb gs://andoshin11-test-bucket 
> Creating gs://andoshin11-test-bucket/...

gs://~以降のBUCKET_NAMEは任意の名前を選択できますが、ユニークである必要があるため既に存在している場合はエラーで怒られます。こちらもGCPコンソールの「Storage」 -> 「ブラウザ」から確認してください

f:id:andoshin11:20180423211058j:plain

Access Tokenの取得

Access Tokenを取得するためにはサービスアカウントの情報が必要です。以下のコマンドでサービスアカウントの情報を格納したJSONが取得できます

$ gcloud iam service-accounts keys create --iam-account test-account@<PROJECT_NAME>.iam.gserviceaccount.com ./test-account.json

CI等からこのアカウントの認証情報を利用するためには、上記で取得したJSONbase64などでエンコードして環境変数に保存すると取り回しがしやすいでしょう。

$ base64 ./test-account.json

// 上記の結果を$CLIENT_SECRETという環境変数に代入した場合は下記のように実行環境でデコード
$ echo $CLIENT_SECRET | base64 --decode > ./client-secret.json

上記のJSONを元にAccess Tokenを取得する方法は以下のとおりです

$ export TEST_ACCOUNT_TOKEN=$(GOOGLE_APPLICATION_CREDENTIALS=./test-account.json gcloud auth application-default print-access-token)

ちなみに自分はFish shellを使っているのでコマンドはこんな感じ

$  export TEST_ACCOUNT_TOKEN=(env GOOGLE_APPLICATION_CREDENTIALS=./test-account.json gcloud auth application-default print-access-token)

Cloud Functionsを拡張する

以下のCloud Functionがあったとします

// index.js
...
exports.echo = function(req, res) {
    res.send('hoge')
}

この関数に認証フローを追加し、以下のように書き換えます

// index.js
...
const Google = require('googleapis');
const BUCKET = '[BUCKET_NAME]'; // 上で選択したBucket Nameに置き換えてください

// リクエストヘッダーからTokenをパース
const getAccessToken = function(header) {
    if (!header) return null;

    const match = header.match(/^Bearer\s+([^\s]+)$/);
    return match ? match[1] : null;
}

// メインで実行したい関数
const authorized = function(res) {
    res.send('hoge')
}

// メイン関数を認証フローでラップした関数
exports.echo = function(req, res) {
    const accessToken = getAccessToken(req.get('Authorization'));
    const oauth = new Google.auth.OAuth2();
    oauth.setCredentials({access_token: accessToken});

    const permission = 'storage.buckets.get'; // 権限の種類
    const gcs = Google.storage('v1');
    gcs.buckets.testIamPermissions(
        {bucket: BUCKET, permissions: [permission], auth: oauth}, {},
        function(err, response) {
            if (response && response['permissions'] && response['permissions'].includes(permission)) {
                authorized(res);
            } else {
                res.status(403).send("不正なアカウントです");
            }
        });
};

上記の関数をデプロイ

Access Tokenを付与してリクエス

まずは愚直に curlを実行してみます。

$ curl https://<your cloud function's URL>
> 不正なアカウントです

エラーで怒られました。

次にAccess Tokenをリクエストヘッダーに付与して実行します。

$ curl https://<your cloud function's URL> -H "Authorization: Bearer "$TEST_ACCOUNT_TOKEN
> 不正なアカウントです

また怒られました...

まぁTest Accountには権限が無いので当然です。下記のコマンドを実行して read権限を追加

$ gsutil acl ch -u test-account@<PROJECT_NAME>.iam.gserviceaccount.com:R gs://<BUCKET_NAME>

もう一度上記のコマンドを実行すると無事にリクエストが通るはず

$ curl https://<your cloud function's URL> -H "Authorization: Bearer "$TEST_ACCOUNT_TOKEN
> hoge

よっしゃ👏👏

まとめ

GCPのIAMは柔軟に権限が設定できる反面、Tokenの発行には若干癖がある印象です。Docker Imageが用意されているのでCIからは問題なく叩けそうですが、それ以外の環境ではやや苦労するかもしれません。

もちろん今回はプロキシとして無理やり認証システムを噛ませているだけなので、認証部分を別の形式に置き換えていただいても問題なし。お好みでどうぞ。

次回は実際にCloud Fuctionsで動くプログラムを紹介します。おつきあいありがとうございました。

*1:ほとんどゴミみたいなBot