Mengamankan akses data untuk pengguna dan grup

Banyak aplikasi kolaboratif yang dapat digunakan pengguna untuk membaca dan menulis bagian data yang berlainan berdasarkan sekumpulan izin. Misalnya, pada aplikasi pengeditan dokumen, pengguna dapat mengizinkan beberapa pengguna untuk membaca dan menulis dokumen mereka dengan tetap memblokir akses yang tidak diinginkan.

Solusi: Role-Based Access Control

Anda dapat memanfaatkan model data Cloud Firestore dan aturan keamanan kustom untuk menerapkan kontrol akses berbasis peran pada aplikasi Anda.

Misalkan Anda membuat aplikasi penulisan kolaboratif yang memungkinkan pengguna membuat "cerita" dan "komentar" dengan persyaratan keamanan berikut:

  • Setiap cerita mempunyai 1 pemilik dan dapat dibagikan dengan "penulis", "pemberi komentar", dan "pembaca".
  • Pembaca hanya bisa melihat cerita dan komentar. Mereka tidak bisa mengedit apa pun.
  • Pemberi komentar memiliki semua akses yang dimiliki pembaca, dan juga dapat menambahkan komentar ke cerita.
  • Penulis memiliki semua akses yang dimiliki pemberi komentar, dan juga dapat mengedit isi cerita.
  • Pemilik dapat mengedit bagian mana pun dalam cerita dan mengontrol akses pengguna lain.

Struktur Data

Misalkan aplikasi Anda memiliki koleksi stories yang setiap dokumennya mewakili sebuah cerita. Setiap cerita juga memiliki subkoleksi comments yang setiap dokumennya merupakan komentar pada cerita tersebut.

Untuk memantau peran akses, tambahkan kolom roles yang merupakan peta ID pengguna ke peran:

/stories/{storyid}

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

Komentar hanya berisi dua kolom, ID pengguna penulis dan sebagian isi:

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

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

Aturan

Setelah peran pengguna terekam dalam database, sekarang Anda perlu menulis Aturan Keamanan untuk memvalidasi peran tersebut. Aturan ini mengasumsikan bahwa aplikasi tersebut menggunakan Firebase Auth, sehingga variabel request.auth.uid adalah ID pengguna.

Langkah 1: Mulai dengan file aturan dasar, yang mencakup aturan kosong untuk cerita dan komentar:

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

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

Langkah 2: Tambahkan aturan write sederhana yang membuat pemilik dapat mengontrol cerita sepenuhnya. Fungsi yang sudah ditetapkan membantu menentukan peran pengguna dan memastikan validitas dokumen baru:

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} {
            // ...
         }
     }
   }
}

Langkah 3: Tulis aturan yang mengizinkan pengguna dengan peran apa pun untuk membaca cerita dan komentar. Gunakan fungsi yang ditetapkan pada langkah sebelumnya agar aturan tetap ringkas dan mudah dibaca:

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']);
        }
     }
   }
}

Langkah 4: Izinkan penulis cerita, pemberi komentar, dan pemilik untuk memposting komentar. Perlu diperhatikan, aturan ini juga memvalidasi bahwa owner komentar cocok dengan pengguna yang memintanya, sehingga pengguna tidak akan saling menimpa komentar satu sama lain:

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;
        }
     }
   }
}

Langkah 5: Izinkan penulis mengedit isi cerita, tetapi tidak untuk mengedit peran cerita atau mengubah properti dokumen lainnya. Hal ini mengharuskan pembagian aturan write cerita menjadi beberapa aturan terpisah untuk create, update, dan delete karena penulis hanya dapat mengupdate cerita:

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;
        }
     }
   }
}

Batasan

Solusi di atas menunjukkan pengamanan data pengguna menggunakan Aturan Keamanan, tetapi Anda harus menyadari batasan berikut:

  • Perincian: Pada contoh di atas, beberapa peran (penulis dan pemilik) memiliki akses tulis ke dokumen yang sama, tetapi dengan batasan yang berbeda. Untuk dokumen yang lebih kompleks, hal tersebut sulit untuk dikelola. Oleh sebab itu, sebaiknya bagi sebuah dokumen menjadi beberapa dokumen yang masing-masing dimiliki oleh satu peran.
  • Grup Besar: Jika Anda perlu berbagi dengan grup yang sangat besar atau kompleks, pertimbangkan sistem yang memungkinkan penyimpanan peran dalam koleksinya masing-masing, bukan sebagai kolom pada dokumen target.