Google Apps ScriptでCloud Functionsに実行制限付でアクセス【GAS】
シリーズでお送りしているわけではないのですが、1つずつが非常に大きなセクションであるため、分割していますが、前回までで「Google Cloud Functionsを使ってパスワードPDF生成」をGASからできるようになりました。しかし、Cloud Functionsはそのままでは、関数実行URLを知ってさえいれば、誰でも実行ができてしまいます。
そこで、他のGoogle CloudのAPIと同様にAccess Tokenを利用した実行制限をつけてみたいと思います。こうする事で、事前に認証用Access Tokenを取得できていなければ、実行が弾かれる仕組みをCloud Functionsの関数に加える事が可能です。
リンク
目次
今回使用するスプレッドシート
前回のサンプルシートにコードを追加し、メニューを追加しています。今回実行するのは、gcf_check関数になります。
事前準備
OAuth2.0認証ライブラリの追加
プロジェクトを移動
サービスアカウントの作成(GCS専用)
※但しこのサービスアカウントは、GCSアクセス専用のサービスアカウントなので、GCFにアクセスするサービスアカウントとは別です。GCF用は別途用意する必要がありますよ。
図:JSONキーファイルをDriveにアップしておく
JSONキーファイルを取得して認証する
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
//認証用各種変数 var tokenurl = "https://accounts.google.com/o/oauth2/token" var json = "ここにJSONキーファイルのIDを入力"; //JSON Keyファイルを指定 //OAuth2認証を実行する function startoauth(){ //UIを取得する var ui = SpreadsheetApp.getUi(); //認証を実行する var service = checkOAuth(); ui.alert("認証が完了し、Access Tokenを取得しました。") } //Google DriveにあるサービスアカウントキーのJSONファイルを取得する function getServiceAccKey(){ //JSONファイルの中身を取得する var content = DriveApp.getFileById(json).getAs("application/json").getDataAsString(); return JSON.parse(content); } //アクセストークン取れてるかどうかチェック function acctokencheck(){ var ui = SpreadsheetApp.getUi(); var service = checkOAuth(); ui.alert(service.getAccessToken()); } //OAuth2.0認証を実行する function checkOAuth() { //JSONファイルの中身を取得する var privateKeys = getServiceAccKey(); return OAuth2.createService('gcf_authorize:' + Session.getActiveUser().getEmail()) //アクセストークンの取得用URLをセット .setTokenUrl(tokenurl) //プライベートキーとクライアントIDをセットする .setPrivateKey(privateKeys['private_key']) .setIssuer(privateKeys['client_email']) //Access Tokenをスクリプトプロパティにセットする .setPropertyStore(PropertiesService.getScriptProperties()) //スコープを設定する .setScope('https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/devstorage.read_write'); } |
Cloud Storageにバケットを用意する
図:役割追加画面
ソースコード
GAS側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
//セキュアアクセスチェック function gcf_check() { //uiを取得 var ui = SpreadsheetApp.getUi(); //Access Tokenを取得(スプレッドシート) var accessToken = ScriptApp.getOAuthToken(); //GCSアクセス用Access Tokenを取得 var service = checkOAuth(); var checkacc = service.getAccessToken(); //スプレッドシート情報を取得する var sheetname = "送信シート"; var sheetid = SpreadsheetApp.openById(ssid).getSheetByName(sheetname).getSheetId(); //送信パラメータを組み立てる var payload = { "key": ssid, "sheetid" : sheetid, "sheetname" : sheetname, "accesstoken": accessToken, //スプレッドシートアクセス用のAccess Tokenだよ "checkacc" : checkacc, //GCSアクセス用Access Tokenだよ "passwd": "tomatoman", //PDFのパスワードを設定します。 }; //POSTで関数を実行する var response = UrlFetchApp.fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + checkacc }, contentType: "application/json", payload : JSON.stringify(payload), muteHttpExceptions: true }); ui.alert(response); } |
- 今回は、前回のコード類は省略しています。認証が通っているかどうかだけを確認します。確認していると、ステータスコード200が返ってくるはずです。(よって、送信パラメータで重要なのは、checkaccの値だけ。
- 認証でアクセス権限(GCS専用サービスアカウント)がなければ、403が返ってきて、アクセス拒否されます。
- 但し実際に前回の記事のコードと併用する場合には、スプレッドシートアクセスのAccess TokenとGCSアクセス用のAccess Tokenの2つが必要になるので、取扱い注意!!
- GCSアクセス用のAccess TokenはBearerとしてheadersに含めて処理をします。
GCF側コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
//追加モジュールを読み込み const Google = require('googleapis').google; const BUCKET = 'ここにバケット名を入れる'; // メインで実行したい関数 function authorized(res) { res.status(200).send("アクセス認証が完了しましたよ。"); } function getAccessToken(header) { if (header) { var match = header.match(/^Bearer\s+([^\s]+)$/); if (match) { return match[1]; } } return null; } exports.secureFunction = function secureFunction(req, res) { var accessToken = getAccessToken(req.get('Authorization')); var oauth = new Google.auth.OAuth2(); oauth.setCredentials({access_token: accessToken}); var permission = 'storage.buckets.get'; var gcs = Google.storage('v1'); gcs.buckets.testIamPermissions( {bucket: BUCKET, permissions: [permission], auth: oauth}, {}, function (err, response) { //レスポンスデータを取得する(JSON形式) try{ if(response != 'undefined'){ //アクセス権限データを参照する var tempdata = response.data.permissions; if(tempdata.includes(permission)){ //アクセス権限があるので、メインの関数を実行する authorized(res); }else{ //403エラーを返す res.status(403).send("アクセスが拒否されました。"); } }else{ //403エラーを返す(レスポンスが空の場合) res.status(403).send("アクセスが拒否されました。"); } }catch(err){ //403エラーを返す res.status(403).send("アクセスが拒否されました。"); } } ); }; |
- 今回はアクセス認証だけを目的としているので、追加モジュールは、googleapisだけです。
- secureFunctionが実行する関数になります。
- リクエストヘッダーからAccess Tokenを取り出し、oauth.setCredentialsにてセットしています。
- GCSへ認証を実行すると、レスポンスの中にstorage.buckets.getのアクセス権のあるものがあれば、認証通過となります。ここでGCS専用サービスアカウントのAccess Tokenがなければ、当然アクセスは拒否されます。
- 認証が通過したら、authorized関数が実行される仕組みなので、前回のコードはこの関数内に記述することで、OAuth2.0認証でラッピングしたCloud Functionsが完成です。
- GCFで実行するサービスアカウントは、GCS専用サービスアカウントではないので注意!!一緒にしてはいけません。
- GCSに対して、サービスアカウントのアクセス権がある場合だけ、dataの中のpermissionsに[storage.buckets.get]の値が入ってるので、これを持って、GCFの実行権の有無を決める。
- GCSのバケット権限からサービスアカウントを削除すれば、403が返り、入れれば200が返ってくる仕組みです。
- Google公式サイトのコードで試してたのですが、なぜか全部403が返ってくるので、上記のようなコードにしています。
- try - catchをつけている理由は、dataにpermissionsがない場合にエラーとなるため、これを捕捉403エラーを返しています。
1 2 3 4 5 6 7 |
{ "name": "sample-http", "version": "0.0.1", "dependencies": { "googleapis": "^39.1.0" } } |
図:package.jsonの内容はこちら
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
"{ "config":{ "url":"https://www.googleapis.com/storage/v1/b/バケット名/iam/testPermissions?permissions=storage.buckets.get", "method":"GET", "headers":{ "Accept-Encoding":"gzip", "User-Agent":"google-api-nodejs-client/0.7.2 (gzip)", "Authorization":"Bearer accessToken", "Accept":"application/json" }, "params":{ "permissions":["storage.buckets.get"] }, "responseType":"json" }, "data":{ "kind":"storage#testIamPermissionsResponse", "permissions":["storage.buckets.get"] }, "headers":{ "alt-svc":"quic=\":443\"; ma=zxxxx; v=\"00,00,00,00\"", "cache-control":"private, max-age=0, must-revalidate, no-transform","connection":"close", "content-length":"96", "content-type":"application/json; charset=UTF-8", "date":"日付", "expires":"失効日", "server":"UploadServer","vary":"Origin, X-Origin", "x-guploader-uploadid":"xxxxxx" }, "status":200, "statusText":"OK" }" |
図:dataの中のpermissionsの有無が重要