Firebase Summit のすべての発表内容に目を通し、Firebase を活用してアプリ開発を加速し、自信を持ってアプリを実行できる方法をご確認ください。 詳細

ユーザーとグループの安全なデータアクセス

多くのコラボレーション アプリでは、ユーザーは一連のアクセス許可に基づいてさまざまなデータの読み取りと書き込みを行うことができます。たとえば、ドキュメント編集アプリでは、不要なアクセスをブロックしながら、少数のユーザーにドキュメントの読み書きを許可したい場合があります。

解決策: 役割ベースのアクセス制御

Cloud Firestore のデータ モデルとカスタムセキュリティ ルールを利用して、ロールベースのアクセス制御をアプリに実装できます。

ユーザーが次のセキュリティ要件で「ストーリー」と「コメント」を作成できる共同執筆アプリケーションを構築しているとします。

  • 各ストーリーには 1 人の所有者がおり、「ライター」、「コメント投稿者」、および「読者」と共有できます。
  • 読者はストーリーとコメントのみを見ることができます。彼らは何も編集できません。
  • コメント投稿者は、読者のすべてのアクセス権を持ち、ストーリーにコメントを追加することもできます。
  • ライターはコメント投稿者のすべてのアクセス権を持ち、ストーリー コンテンツを編集することもできます。
  • 所有者は、ストーリーの任意の部分を編集したり、他のユーザーのアクセスを制御したりできます。

データ構造

アプリに、各ドキュメントがストーリーを表すstoriesコレクションがあるとします。各ストーリーにはcommentsサブコレクションもあり、各ドキュメントはそのストーリーに対するコメントです。

アクセス ロールを追跡するには、ロールへのユーザー ID のマップであるrolesフィールドを追加します。

/stories/{ストーリーID}

{
  title: "A Great Story",
  content: "Once upon a time ...",
  roles: {
    alice: "owner",
    bob: "reader",
    david: "writer",
    jane: "commenter"
    // ...
  }
}

コメントには、作成者のユーザー ID とコンテンツの 2 つのフィールドのみが含まれます。

/stories/{storyid}/comments/{commentid}

{
  user: "alice",
  content: "I think this is a great story!"
}

ルール

ユーザーの役割がデータベースに記録されたので、それらを検証するセキュリティ ルールを作成する必要があります。これらのルールは、 request.auth.uid変数がユーザーの ID になるように、アプリがFirebase Authを使用していることを前提としています。

ステップ 1 : ストーリーとコメントの空のルールを含む基本的なルール ファイルから始めます。

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
         // TODO: Story rules go here...

         match /comments/{comment} {
            // TODO: Comment rules go here...
         }
     }
   }
}

ステップ 2 : 所有者がストーリーを完全に制御できるようにする単純なwriteルールを追加します。定義された関数は、ユーザーの役割を決定し、新しいドキュメントが有効かどうかを決定するのに役立ちます。

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          // Read from the "roles" map in the resource (rsc).
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          // Determine if the user is one of an array of roles
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          // Valid if story does not exist and the new story has the correct owner.
          return resource == null && isOneOfRoles(request.resource, ['owner']);
        }

        // Owners can read, write, and delete stories
        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

         match /comments/{comment} {
            // ...
         }
     }
   }
}

ステップ 3 : 任意のロールのユーザーがストーリーとコメントを読むことを許可するルールを作成します。前のステップで定義した関数を使用すると、ルールが簡潔で読みやすくなります。

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner']);

        // Any role can read stories.
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          // Any role can read comments.
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
        }
     }
   }
}

ステップ 4 : ストーリー ライター、コメント投稿者、および所有者がコメントを投稿できるようにします。このルールは、コメントのownerが要求しているユーザーと一致することも検証することに注意してください。これにより、ユーザーが互いのコメントを上書きすることが防止されます。

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return resource == null
            && request.resource.data.roles[request.auth.uid] == 'owner';
        }

        allow write: if isValidNewStory() || isOneOfRoles(resource, ['owner'])
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);

          // Owners, writers, and commenters can create comments. The
          // user id in the comment document must match the requesting
          // user's id.
          //
          // Note: we have to use get() here to retrieve the story
          // document so that we can check the user's role.
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

ステップ 5 : ライターにストーリー コンテンツを編集する権限を与えますが、ストーリーの役割を編集したり、ドキュメントの他のプロパティを変更したりすることはできません。ライターはストーリーのみを更新できるため、ストーリーのwriteルールをcreateupdate 、およびdeleteの個別のルールに分割する必要があります。

service cloud.firestore {
   match /databases/{database}/documents {
     match /stories/{story} {
        function isSignedIn() {
          return request.auth != null;
        }

        function getRole(rsc) {
          return rsc.data.roles[request.auth.uid];
        }

        function isOneOfRoles(rsc, array) {
          return isSignedIn() && (getRole(rsc) in array);
        }

        function isValidNewStory() {
          return request.resource.data.roles[request.auth.uid] == 'owner';
        }

        function onlyContentChanged() {
          // Ensure that title and roles are unchanged and that no new
          // fields are added to the document.
          return request.resource.data.title == resource.data.title
            && request.resource.data.roles == resource.data.roles
            && request.resource.data.keys() == resource.data.keys();
        }

        // Split writing into creation, deletion, and updating. Only an
        // owner can create or delete a story but a writer can update
        // story content.
        allow create: if isValidNewStory();
        allow delete: if isOneOfRoles(resource, ['owner']);
        allow update: if isOneOfRoles(resource, ['owner'])
                      || (isOneOfRoles(resource, ['writer']) && onlyContentChanged());
        allow read: if isOneOfRoles(resource, ['owner', 'writer', 'commenter', 'reader']);

        match /comments/{comment} {
          allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                      ['owner', 'writer', 'commenter', 'reader']);
          allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
                                        ['owner', 'writer', 'commenter'])
                        && request.resource.data.user == request.auth.uid;
        }
     }
   }
}

制限事項

上記のソリューションは、セキュリティ ルールを使用してユーザー データを保護する方法を示していますが、次の制限事項に注意する必要があります。

  • 粒度: 上記の例では、複数の役割 (ライターと所有者) が同じドキュメントへの書き込みアクセス権を持っていますが、制限は異なります。これは、より複雑なドキュメントでは管理が難しくなる可能性があり、単一のドキュメントを、それぞれが単一のロールによって所有される複数のドキュメントに分割することをお勧めします。
  • 大規模なグループ: 非常に大規模または複雑なグループと共有する必要がある場合は、ロールがターゲット ドキュメントのフィールドとしてではなく、独自のコレクションに格納されるシステムを検討してください。