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キーファイルを取得して認証する
//認証用各種変数
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側コード
//セキュアアクセスチェック
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側コード
//追加モジュールを読み込み
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エラーを返しています。
{
"name": "sample-http",
"version": "0.0.1",
"dependencies": {
"googleapis": "^39.1.0"
}
}
図:package.jsonの内容はこちら
"{
"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の有無が重要








