Firestoreで権限の管理をする方法

2022/03/14に公開

概要

Firestoreで権限の管理をする方法について調べたので記事にしました。
実装はDartですが、どの言語でも同じようにできそうです。
イメージとしては、チャットアプリの書き込み、読み込み権限を管理するコレクションって感じです。
(アプリ的には書き込めるけど読み込めないとかはありえないけど、そのへんはサンプルなので適当です...)
権限を管理するコレクションの名前はroom_rolesとしました。
以下の画像がDBの実データになります。

DB

room_rolesはrolesというフィールドに、ユーザーIDをキーにして、権限のリストを持っています。
たとえば、user1が読み込み権限を持ってた場合には、roles: {user1: ["read"]}という感じのデータ構造になります。

本記事では、以下について解説します。

  1. room_rolesドキュメントから特定の権限を持つユーザ一覧を取得する
  2. あるユーザーについて特定の権限を持つroom一覧を取得する(読み込み権限のある部屋一覧など)
  3. room_rolesに権限を付与・剥奪する

Roleクラス

room_rolesを表現するようなRoleクラスを定義してみます。

class Role {
  final String id;
  final String name;
  final Map<String, List<String>> roles;

  Role({
    required this.id,
    required this.name,
    required this.roles,
  });

  Map<String, dynamic> toJson() {
    return {
      "id": id,
      "roles": roles,
      "name": name,
    };
  }

  Iterable<String> userIds(String role) {
    return roles.entries.where((r) => r.value.contains(role)).map((r) => r.key);
  }
}

RoleにはuserId(String role)メソッドを定義したので、roleを渡して権限を持つユーザーID一覧を取得出来ます。

final roomRole = Role(id: "1", name: "room1", roles: {
  "user1": ["write"],
  "user2": ["read"],
  "user3": ["write"],
});
 
print("> room1 に write 権限があるのは以下のユーザーです");
print(roomRole.userIds("write"));
> room1 に write 権限があるのは以下のユーザーです
(user1, user3)

これが1.のroom_rolesドキュメントから特定の権限を持つユーザ一覧を取得する方法です。
ここまではFirestoreは関係ないですね。
room_rolesドキュメントさえ持っていれば簡単に権限の確認ができます。

あるユーザーについて特定の権限を持つroom一覧を取得する

チャットルーム一覧を取得する場合に、読み込み権限を持つ部屋の一覧を取得することになります。
このようなクエリをFirestoreでは次のように書くことが出来ます。

Future<Iterable<String>> findRoom(String userId, String role) async {
  final query = FirebaseFirestore.instance
      .collection("room_roles")
      .where("roles.$userId", arrayContains: role);
  final snapshot = await query.get();

  return snapshot.docs.map((e) => e.data()["name"]);
}

この関数はuserIdとroleを引数にとり、room名を返します。
この関数のポイントはmap型のrolesのuserIdに対してarrayContainsを使っていることです。
こう書くことで、map型に対してもクエリすることが出来ます。しかも配列のvalueに対してarrayContainクエリを使っています。

実際に試してみるまで、このようなクエリができることを知らなかったので、これが一番の発見でした!

room名を返しているのはデモ用表示のためで、実際に使う際にはroom_rolesにroom_idをもたせておいてroom_idを返すなり、Roleクラスを返すなりすることになると思います。

findRoom関数を使うことで、以下のように、自分の読み込める部屋や書き込める部屋一覧を取得することができます。

Future<void> printRole(String userId, String role) async {
  print("> $userId$role 権限を持つのは以下の部屋です");
  final names = await findRoom(userId, role);
  print(names);
}

await printRole("user1", "read");
> user1 が read 権限を持つのは以下の部屋です
(room2)

権限の付与・剥奪

権限の付与と剥奪はrolesのuserIdにFieldValue.arrayUnion(roles), FieldValue.arrayRemove(roles)を使うことで、任意のroleをつけたり消したりすることが出来ます。

Future<void> addRole({
  required String roleId,
  required String userId,
  required List<String> roles,
}) async {
  await FirebaseFirestore.instance
      .collection("room_roles")
      .doc(roleId)
      .update({"roles.$userId": FieldValue.arrayUnion(roles)});
}

Future<void> removeRole({
  required String roleId,
  required String userId,
  required List<String> roles,
}) async {
  await FirebaseFirestore.instance
      .collection("room_roles")
      .doc(roleId)
      .update({"roles.$userId": FieldValue.arrayRemove(roles)});
}

FieldValue.arrayUnionはキーが重複しないように値を追加してくれるので、user1: ["write"]みたいなデータにFieldValue.arrayUnion(["write"])してもuser1: ["write", "write"]のようになりませんでした。
これも初めて知ったので、今後使っていきたいです。

おわり

コードの全体はここに載せておきます。
セキュリティルールについてはこのページを参考にすると作れそうです。
時間があればセキュリティルールについても書きたいです。

Discussion