ChatGPTを使って、Firestoreのセキュリティールールを作ってみた

2023/06/01に公開

こんにちは、フットボール・テクノロジーズ社でフロントエンジニアとして働いているpeko-pekoです。Zennでの2本目の投稿になります😆
今回は、業務委託で開発しているサービスの中で、FirestoreのセキュリティールールをChatGPTと一緒に会話しながら作ったので、整理も兼ねて記事にしたいと思います💪

tl;dr

  1. Firestoreのセキュリティールールを、ChatGPTと一緒に会話しながら作ってみた
  2. 完成度としては80%程度。20%は手動で修正が必要
  3. ChatGPTの出力する文字数制限があるので、コピペが大変😭

やったこと

Firebaseのfirestoreを利用する際に大事なのがセキュリティールールをガッチリ作ること。
過去には自力で作っていたが、ChatGPTを使ってセキュリティールールを自然言語で書けないか?という疑問から、下記のフローでルールとテストを作ってみた。

ChatGPTとの会話のフロー

  1. ユーザーストーリーを自然言語で書く。サンプルコードを少し書く。
  2. 1をChatGPTに投げて、セキュリティールールを作ってもらう
  3. 2の完成度が90%程度なので、手動で修正
  4. 3をChatGPTに投げて、セキュリティールールのテストを作ってもらう
  5. 4の完成度が70%程度なので、手動で修正
  6. 5のテストを実行して、全てがPassすることを確認

最初にChatGPTに投げたprompt

実際に作成したセキュリティーコードは、collectionの数だけあるので膨大な行数になりました。
ここでは、例として一般的な下記のコレクションを想定したコードを共有します。

説明用のサンプルDB

コレクション名
users: /users/{userId} 
{
  displayName: "peko-peko",
  icon: "https://xxxxx/peko-peko.jpg"
} 
=> 全ての人が閲覧できる

-------

コレクション名
secrets: /users/{userId}/secrets/{secretId}
{
  email: "peko-peko@gmail.com"
}
=> Adminと本人のみが、閲覧できる

上記のようにUserコレクションとサブコレクションであるSecretコレクションが存在するとします。Userは誰でも閲覧可能ですが、Secretsは本人とAdmin管理者のみが閲覧できます。

これらのセキュリティールールを作るときに、下記のpromptをChatGPTに投げます。
ポイントは、サンプルコードを一緒に投げることで、想定したoutputの形になりやすいです。ただし、inputの文字数制限があるので、その辺りの調整が必要。

firebaseのfirestoreのセキュリティルールを作ってください
下記の要件を満たすようにしてください

### 要件
- パス:/users/{userId}
    - read
        - ログインしていない場合
            - user が表示できる
        - ログインしている場合
            - user が表示できる
    - create
        - ログインしていない場合
            - user が作成できない
        - ログインしている場合
            - admin の場合
                - user が作成できる
            - admin ではない場合
                - user が作成できない
    - update
        - ログインしていない場合
            - user が更新できない
        - ログインしている場合
            - admin の場合
                - user が更新できる
            - admin ではない場合
                - userId === request.auth.token.userId の場合
                    - user が更新できる
                - userId !== request.auth.token.userId の場合
                    - user が更新できない
- パス:/users/{userId}/secrets/{secretId}
    - read
        - ログインしていない場合
            - secret が表示できない
        - ログインしている場合
            - admin の場合
                - secret が表示できる
            - admin ではない場合
                - secretId === request.auth.token.userId の場合
                    - secret が表示できる
                - secretId !== request.auth.token.userId の場合
                    - secret が表示できない
    - create
        - ログインしていない場合
            - secret が作成できない
        - ログインしている場合
            - admin の場合
                - secret が作成できる
            - admin ではない場合
                - secret が作成できない
    - update
        - ログインしていない場合
            - secret が更新できない
        - ログインしている場合
            - admin の場合
                - secret が更新できる
            - admin ではない場合
                - secretId === request.auth.token.userId の場合
                    - secret が更新できる
                - secretId !== request.auth.token.userId の場合
                    - secret が更新できない

### サンプルコード
function isAuthenticated() {
  return request.auth != null;
}

function isAdmin() {
  return isAuthenticated() && request.auth.token.userType == 1;
}

match /users/{userId} {
allow read;
allow create: if isAdmin();
allow update: if isAdmin() || request.auth.token.userId == userId;
}

セキュリティールールのテストを作成するprompt

上記で生成されたセキュリティールールのcode全てをpromptに投げます。
ポイントは、ここでもサンプルコードを追加で投げることで、思ったようなoutputを得られる確率が上がります。

Firestoreのセキュリティールールがあります。
このテストを作成してください。

### セキュリティールール
ここに1で作成したルールを貼り付ける

### サンプルコード
 describe("users/{userId}", () => {
    describe("READ", () => {
      test("全員 が読み込み可能", async () => {
        await initDB("users/sato", { displayName: "佐藤", userType: 3 });
        const db = await logoutAuth();
        const ref = doc(db, "users/sato");
        await assertSucceeds(getDoc(ref));
      });
    });

    describe("CREATE", () => {
      test("Admin のみが作成可能", async () => {
        const db = await loginAuth({
          uid: "admin",
          userType: 1
        });
        const ref = doc(db, "users/user1");
        await assertSucceeds(setDoc(ref, { displayName: "佐藤" }));
      });

      test("未ログイン は作成不可", async () => {
        const db = await logoutAuth();
        const ref = doc(db, "users/user1");
        await assertFails(setDoc(ref, { displayName: "佐藤" }));
      });

      test("seller は作成不可", async () => {
        const db = await loginAuth({
          uid: "seller",
          userId: "user1",
          userType: 2
        });
        const ref = doc(db, "users/user1");
        await assertFails(setDoc(ref, { displayName: "佐藤" }));
      });

      test("buyer は作成不可", async () => {
        const db = await loginAuth({
          uid: "buyer",
          userId: "user1",
          userType: 3
        });
        const ref = doc(db, "users/user1");
        await assertFails(setDoc(ref, { displayName: "佐藤" }));
      });
    });
  });

難しかった点

冒頭にも書いた通り、想定したoutputの80%程度の完成度になりました。特に、firestoreのDB構造(サブコレクション)などの下記を、ChatGPTに理解してもらうのが難しかった。

  1. CollectionGroupで取得するコレクションのObject構造を理解させる
  2. pathからのuserIdと、DBからのresource.data.user.id を区別させる。特にCollectionGroupの場合!
  3. custom claims の値を使う
  4. 大量のrulesを作ってもらうと、しれっと平気で嘘を書くことがある
  5. 同じpromptを投げても、毎回違う結果になるので、PDCAが回しづらい(継続的な少しづつの修正がしづらい)

今後の運用方法

運用していると、collectionが増えたり、rulesの中身を修正することがあります。
そんな時は、既に完成したrules、testをChatGPTに投げて理解させた上で、追加 or 修正の差分をChatGPTに投げることでoutputの精度が上がることが分かりました。

具体的には、、、

  1. 既存のrulesをChatGPTに投げる
  2. 既存のtestをChatGPTに投げる
  3. 新たに追加したい差分のみを投げて、outputを依頼する

3の例。

既存のセキュリティールールに、下記のcollectionを追加したいです。
セキュリティールールとテストを書いてください。

### 追加
- パス:/users/{userId}/likes/${likeId}
    - read
        - ログインしていない場合
            - like が表示できる
        - ログインしている場合
            - like が表示できる
	    
...

最後に。。。

今回、ChatGPTにセキュリティールールを作ってもらった。promptの調整次第でさらに精度が上がると感じたが、同時に、対象コレクションが増えると、どうしても文字数制限に引っかかる問題が発生する。コレクションを分割して投げるなどの工夫が必要であり、手動でのコピペの回数が増えるのは悩ましいところ。
今後は、APIを利用して機械的にテキストを分割して、最後に集約するような形に調整したいと思います💪

最後に、株式会社フットボール・テクノロジーズでは、Firebaseを利用したサービスを業務委託として数多く開発しています。興味がある方がいましたら、下記よりコンタクトをお願いします🙇‍♂️
https://delicious-mist-3a0.notion.site/4a54c218dd37480483d09af8dc1a9e69

今日もお腹がペコペコ。

Discussion