l12a

白ウサギを追え

Firebase で公開するウェブサイトに「管理者機能」を付ける

Firebase + Vue.js で ポートフォリオサイト を制作/公開しています。

f:id:lnly:20190122100936p:plain
公開中のポートフォリオサイト

このポートフォリオサイトのコンテンツをコード上に直書きするのではなく、管理者専用ページから Firebase Firestore と Storage にデータや画像をアップロードして反映できるようにしました。
ページ上で言うと、「 Skills 」と「 Portfolios 」のコンテンツがそれに当たります。

f:id:lnly:20190125010806p:plain
Firestore と Storage で管理するコンテンツ

管理者専用ページへのアクセス制御と、 Firestore と Storage のデータ書き込み制限を実装するために行ったことをまとめます。
主に Firebase の設定周辺に言及する内容で、UI 側の構築方法についてはほぼ触れていません。
またこの記事では Firebase Authentication を使ってのユーザ認証はできていることを前提として、それ自体についても割愛します。

任意のユーザを管理者にする

ユーザに Custom User Claims をセットして「管理者」であると識別できるようにします。

Custom User Claims をセットする Cloud fuctions を登録する

Custom User Claims はウェブで通常使用するクライアント SDK ではなく Admin SDK を用いて、かつ特権サーバからセットする必要があるので、 Cloud Functions を使います。

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

admin.initializeApp(functions.config().firebase);

exports.addAdminClaim = functions.firestore.document('admin_users/{docID}').onCreate((snap) => {
    const newAdminUser = snap.data();
  if (newAdminUser === undefined) return;
  modifyAdmin(newAdminUser.uid, true);
});

exports.removeAdminClaim = functions.firestore.document('admin_users/{docID}').onDelete((snap) => {
  const deletedAdminUser = snap.data();
  if (deletedAdminUser === undefined) return;
  modifyAdmin(deletedAdminUser.uid, false);
});

function modifyAdmin (uid: string, isAdmin: boolean) {
  admin.auth().setCustomUserClaims(uid, {admin: isAdmin}).then(() => {
    // The new custom claims will propagate to the user's ID token the
    // next time a new one is issued.
});
}

これで Firestore のコレクション admin_users へドキュメントが追加されたり削除されたことをトリガーとして、 任意のユーザを管理者として識別するための Custom User Claims をセットしたり、またこれを無効にすることができるようになりました。

UID を入手する

Custom User Claims をセットするには、対象となるユーザの UID が必要です。
今回は Firebase コンソールから直接入手しました。

f:id:lnly:20190125011301p:plain
Firebase コンソール画面で対象のユーザの UID を入手

Cloud Functions を経由して管理者権限を付ける

入手した UID は Firestore に保存します。
コレクション admin_users にドキュメントを作成してフィールド uid を書き込みます。

f:id:lnly:20190125012158p:plain
管理者を Firestore に登録する

作成したコレクションにはセキュリティルールで read / write の権限を許可する必要はありません。
このコレクションにアクセスするのは先に登録した Cloud Functions のみで、 Cloud Functions は特権サーバのためセキュリティルールを設定していなくてもドキュメントを参照できるからです。

他のユーザにさらに管理者権限を付与したいときは同様にドキュメントを追加して UID を書き込めば自動的に Custom User Claims が書き込まれ、また管理者権限を剥奪したいときは対象のユーザの UID をフィールドにもつドキュメントを削除すれば自動的に行えます。

管理者を判別して画面表示を制御する

ウェブページ上で管理者だけが表示できるコンテンツを作るために管理者を判別するには、Firebase クライアント SDK を使い、

import * as firebase from 'firebase/app';
import firebaseConfig from './path/to/your/firebaseConfig';

firebase.initializeApp(firebaseConfig);

firebase.auth().onAuthStateChanged((user) => {
  if (user) {
    user.getIdTokenResult(true).then((idTokenResult) => {
      if (idTokenResult.claims.admin) {
        // current user has admin
      }
    });
  }
});

このように判定しました。
クライアントサイドスクリプトで上記のコードを使ってコンテンツの表示やルーティングの制御が可能です。

セキュリティルールの変更

Custom User Claims を使用して、管理者だけが表示できるコンテンツを作ることができましたが、クライアントサイドスクリプトを使ってコンテンツの表示/非表示を制御するだけではなくセキュリティルールを編集して Firestore と Storage にも管理者だけが書き込めるようにします。

Firestore のセキュリティルール

service cloud.firestore {
  match /databases/{database}/documents {
    match /portfolios/{document=**} {
      allow read;
      allow write: if request.auth.token.admin == true;
    }
    match /skills/{document=**} {
      allow read;
      allow write: if request.auth.token.admin == true;
    }
  }
}

このようなルールにしました。

Storage のセキュリティルール

service firebase.storage {
  match /b/{bucket}/o {
    match /portfolios {
      match /{allImages=**} {
        allow read;
        allow write: if request.auth.token.admin == true;
      }
    }
  }
}

このようにしました。
ここまでの内容で、管理者専用ページへのアクセス制限と、 Firestore と Storage の書き込み制御ができました。それぞれ read 権限は全開放しているので、ユーザ認証をしていない接続者に対しても画面上で表示することができます。

参考

管理者機能の実装は、公式ドキュメントを参考に行いました