firestoreのセキュリティルールでグループチャットルームを考察

firebase ロゴ firebase

firestore本当に便利ですね。
いうなればフロントからクエリを書いて、データを入力したり、データを読み出したりできるので、旧来のようにデータをPOSTして、サーバ側でクエリを生成するような手間が無くて助かります。

ただし、フロントでクエリを書くってことはやっぱり心配なのがセキュリティです。
言ってしまえばフロントから好きなようにDBを操作できてしまうのは恐ろしいことです。

FireStoreのセキュリティ・ルールを使えばいいじゃない

はい、その通り。 FireStoreはフロントの強い味方です。今までサーバでやってたユーザ認証や権限の確認といったものはこのFireStore セキュリティルールが一括して面倒を見てくれるようになりました。
JSON風に記述できて、慣れれば結構簡単です。(慣れるまでは暗号にしか見えません)
実際いろんなことができます。
FirebaseのAuth機能と連携して、アクセスし用としているユーザのUIDをチェックしたい場合はrequest.auth.uid でユーザのUIDが取得できます。
詳しいガイドは公式が詳しく説明しているのでそちらをご覧ください。

セキュリティルールでチャットルームを作ってみたい

グループチャットのような機能が必要でした。
更に言えば、グループチャットルームは招待制で、パスワードが無いと入室できません。
チャットルーム内のデータはメンバー以外一切アクセスできないとします。
メンバーは、ルーム内にmembers{uid:name , uid:name }のように書いていくとします。

まず、セキュリティルールその1 ルームのメンバーのみ、データへのアクセスを許可する

service cloud.firestore {
  match /databases/{database}/documents {
    // ルームは /rooms/{roomId}のmembersにuidがあるものだけアクセス可能
    match /rooms/{roomId} {
      function isMember() {
        // roomドキュメント内のmembersにアクセス者のUIDが存在するか?
        return request.auth.uid in get(/databases/$(database)/documents/rooms/$(roomId)).data.members
      }

      allow read,write : if isMember()

      match /someChildCollections/{docs=**}{
     	 allow read,write : if isMember()
      }

    }
  }
}

これでルームメンバー以外ルーム内のデータにアクセスできなくなります。
しかしパラドックスが・・・

チャットルームを最初に作るとき、メンバー不在なので作れません

これね。
firebase.firestore().collection(‘rooms’).doc.set(roomParam)
とか書くとセキュリティルールではじかれてしまいます。
今から作ろうとしているドキュメントに、すでにメンバーがいないと作れないというパラドックス。
ちょっと前に流行りましたね。

卵が先か、鶏が先か

という状態になります。

解決策は、公式サイトにある exists()を使って、チャットルームがない場合書き込みできるような条件を作るか、cloud functionsを使うかのどちらかになると思います。
あるいはもっと賢いDB設計があるのかもしれません。世の中は天才的な解決策を持つ人がたくさんいらっしゃるのできっと素晴らしい解法があるはずです。
ともかく一人で作っているのでこの解決は独自に考えるしかありません。
そして今回とった対策はCloud functionsを使ってFirestoreにデータを書き込むという方法でした

Cloud functionsでFireStoreにアクセス

この、Cloud functionsからFirestoreにアクセスすると、セキュリティルールの適用除外になります。
フロントからの好き勝手なクエリから守るためのセキュリティルールなので、そもそもサーバからfireStoreへのアクセスはセキュリティルールが
要らないのも理に適ってますね。
httpでアクセスできるように作ります。Expressを使ってもいいみたいなので今回はExpress4を採用してみます。

import * as functions from 'firebase-functions'
const cors = require("cors")
const express = require("express")
const bodyParser = require('body-parser')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)
// expreess 初期設定
const app = express()

function validateFirebaseIdToken(req, res, next) {
  console.log("firebase正規ユーザ確認用Middle ware");

  if (!req.headers.authorization || !req.headers.authorization.startsWith("Bearer ")) {
    console.error("No Firebase ID token was passed as a Bearer token in the Authorization header.");
    res.status(403).send("Unauthorized you not user");
    return;
  }

  let idToken;
  if (req.headers.authorization && req.headers.authorization.startsWith("Bearer ")) {
    console.log("Found 'Authorization' header");
    idToken = req.headers.authorization.split("Bearer ")[1];
  }

  admin.auth().verifyIdToken(idToken).then(decodedIdToken => {
    console.log("ID Token correctly decoded", decodedIdToken)
    req.user = decodedIdToken
    next()
  }).catch(error => {
    console.error("Error while verifying Firebase ID token:", error);
    res.status(403).send("Unauthorized");
  });
}

// express ミドルウェア
app.use(cors({ origin: true }))
app.use(validateFirebaseIdToken) // これは公式のサンプルに載ってますが、Firebaseの正規ユーザかチェックしてくれます
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())

app.post('/',async(req,res)=>{
  
  const roomRef = admin.firestore().collection('rooms').doc()
  const roomParam = {
    name:req.body.roomName,
    members:{
      [req.user.uid]:{
        name:req.body.yourName,
        rank:'admin'
      }
    }
  }
  const batch = admin.firestore().batch()
  // ルームの作成
  //同時に書き込みたいことがあればバッチで
  
  batch.set(roomRef,roomParam)
  try{
    await batch.commit()
    res.status(200).send('success')
  }catch(e){
    console.error('ルーム作成に失敗')
    console.error(e)
    res.status(500).send("error");
  }

})



const api = functions.https.onRequest(app)

module.exports = {
  api
}



とこのように、Cloud FUnctionsを使えばセキュリティルールの壁を抜けることができます。
exists()とどちらがいいかはお好みや用途にもよるかと思います。今回の仕組みではrooms以外にusersコレクションにもデータを追記したかったのです。
usersはusersで、自分自身しかデータが書き換えできないのでルームに招待されたときにセキュリティルールの壁がまた発動すると思ったので、
cloud functionsを使ってみました。

Cloud functionsはまだ公開から半年程度しか経っていないため、まだ情報が少ないですね。早く情報やノウハウがたくさん出てくるといいなぁ