やりたいこと:
Google認証連携(JIT Provisioning)してるサービス側の管理者が、定期的に削除アカウントの棚卸しをしたいとのことなので、Cloud Functionに口を作って日時でユーザーを取れるWeb APIを用意したい。
(スクラッチのサービスなのでProvisioningのつなぎこみがない)
→ Javascript / Node.jsの勉強がてら作ってみる
Admin SDKの有効化
- Cloud Console API Libraryに行く
- 対象のプロジェクトを選択
- APIライブラリでAdmin SDKを検索 → 有効にする
Admin SDK叩くための認証情報発行
- OAuthクライアント
- OAuth同意画面を設定しておく
- 認証情報の作成で、Admin SDKを選択、OAuthクライアントIDを作成
- 認証情報をJson形式でダウンロード
- 公式のサンプルをローカルで叩いて、Refresh Tokenをゲット
- Json内のClient ID, Client Secret, 取得したRefresh Tokenを使って都度Access Tokenをゲットできる
※一応Refresh Tokenも6ヶ月使用がなかったりすると死ぬけど、明示的にRevokeしない限り使えるものとして考えててよさそう
※シンプルAPIキーはNGらしい
コードの中身
とりあえずローカルで動かせるコード。
Cloud Functionに上げる時は、Secretsの格納方法を検討しつつ、responseの処理を追記。(SecretsはStorageかなぁ。あと公開範囲の制限も検討)
const { promisify } = require('util');
const request = require('request');
const reqProm = promisify(request);
const cred = {
custometId: '{一回サンプルコード実行したレスポンスの中から取得可能}', //単一ドメイン環境ならドメインでも可
client_id: '{前段階で取得したやつ}',
client_secret: '{前段階で取得したやつ}',
refresh_token: '{前段階で取得したやつ}',
};
const getAccessToken = async (cred) => {
options = {
url: 'https://accounts.google.com/o/oauth2/token',
method: 'POST',
headers: '',
qs: {
client_id: cred.client_id,
client_secret: cred.client_secret,
refresh_token: cred.refresh_token,
grant_type: 'refresh_token'
}
};
const token = await reqProm(options).then(data => {return JSON.parse(data.body).access_token});
return token;
};
const listUsers = async (token, cred) => {
const options = {
url: 'https://www.googleapis.com/admin/directory/v1/users',
method: 'GET',
headers: {
Authorization: 'OAuth ' + token
},
qs: {
customer: cred.custometId, //単一ドメイン環境でドメイン指定で取得する場合は、customerじゃなくてdomainを使う
showDeleted: 'true',
orderBy: 'email'
}
};
const response = await reqProm(options).then(data => {return data});
return response;
};
const getDeletedUserList = async (cred) => {
const thisTime = new Date();
const today = new Date(thisTime.getFullYear(), thisTime.getMonth(), thisTime.getDate());
const yesterday = new Date(thisTime.getFullYear(), thisTime.getMonth(), thisTime.getDate()-1);
let userlist = [];
const token = await getAccessToken(cred);
let result = await listUsers(token, cred);
if (result.statusCode == 200) {
const users = JSON.parse(result.body).users;
//昨日削除されたユーザーのみ抽出
let filtered_users = users.filter(function(element) {
let deletionTime = new Date(element.deletionTime);
return deletionTime >= yesterday && deletionTime < today
});
filtered_users.forEach(user => {
userlist.push(user.primaryEmail);
});
console.log(userlist); //ここでresponseを返す
} else {
console.log(JSON.parse(result.body).error.message);
};
};
//exports.mainに相当する箇所。関数の呼び出しのみ。
//resは呼び出した関数の中で処理する
getDeletedUserList(cred);
[
'aaa@xxx.com',
'bbb@xxx.com',
'ccc@xxx.com',
]
※googleのサンプルコード参考にしてたら非同期処理に引っかかって想定の動作にならないわレファレンス探すのクッソ大変だわでrequestで実装(後から調べたらpromisifyするんじゃなくてrequest-promiseで良かったなこれ)。callback地獄の辛さと非同期処理の勉強にはなった。
非同期の関数(requestとかもろもろ)をシーケンシャルに処理するには、まとめて実行するためのasync関数作ってその中でawaitで処理すべし、ということが今回の教訓。
例えば以下のように書くと死ぬ。
const getAccesstoken = async (aaa) => {
...(request処理)...
return accessToken
};
const getUserList = async (bbb) => {
...(request処理)...
return userList
};
const accessToken = getAccesstoken(aaa);
const userList = getUserList(accessToken);
console.log(userList); // Promise{ <pending> } 等になる
pythonとかの感覚で、返ってきたuserListをmainの中でこねこねしてレスポンスで返すぞ、とするとPromiseの処理がキュー的に後回しになってるため無事死亡。
それも含めてシーケンシャルにやりたければこうする↓
const getAccesstoken = async (aaa) => {
...(request処理)...
return accessToken;
};
const getUserList = async (bbb) => {
...(request処理)...
return userList;
};
const main = async () => {
const accessToken = await getAccesstoken(aaa);
const userList = await getUserList(accessToken);
console.log(userList); // ちゃんとreturnされたlistが出る
}
main();
awaitでPromiseの処理が完了するまで待ってあげればシーケンシャルに処理される。
なお、callback関数を途中で混ぜると結局死ぬ。(awaitで呼び出した関数の中でcallbackした時点でawaitの束縛外れるので、シーケンシャルに処理したければひたすらネストしてくしかない。Googleのサンプルコードはcallback呼び出してたので、ここがよくわからずハマってた。しかもGoogleのサンプルコードだとGoogle作成のクラス使ってて、promisifyもうまくいかなかったのでどうしようもなかった。)
Promise || async/awaitは例えばいろんなDBから並列で情報とってくるみたいなケースだと非常に有効だが、pythonとかから入った身からすると理解するまでが厳しかった。
その他async/await, promise周りいろいろ
#(おまけ)Cloud Functionをローカルでメンテナンス
- gcloudでローカルに環境作る
- そもそもpython2.7.xが必要なので作業フォルダにpyenvでインストール&pyenv localで2.7.xを指定する
- gcloud auth loginでログイン
- gcloud functions listで対象のプロジェクトに入ってるかチェック
- gcloudでデプロイ
- pyenvで
.python-version
ファイルができてるので、.gcloudignore
作ってアップされないようにしとく - package.json作っとく
- gcloud deployでプロジェクトを新規作成。(※この際に
--region=asia-northeast1
あたり指定しておくのがよさげ。特にデフォのリージョンでよければいらない)
- pyenvで
参考ページ
Google Admin SDK周り
Node.js周り
gcloud周り
(GASで作ってAPI公開しろ? 全くその通り)