Many collaborative apps allow users to read and write different pieces of data based on a set of permissions. In a document editing app, for example, users may want to allow a few users to read and write their documents while blocking unwanted access.
Решение: Управление доступом на основе ролей.
Вы можете использовать модель данных Cloud Firestore, а также настраиваемые правила безопасности для реализации управления доступом на основе ролей в вашем приложении.
Предположим, вы разрабатываете приложение для совместного написания текстов, в котором пользователи могут создавать «истории» и «комментарии» со следующими требованиями к безопасности:
- У каждой истории есть один владелец, и ею можно поделиться с «авторами», «комментаторами» и «читателями».
- Читатели могут видеть только статьи и комментарии. Они не могут ничего редактировать.
- Комментаторы имеют полный доступ ко всей информации, что и читатели, и могут также оставлять комментарии к статье.
- Авторы имеют полный доступ ко всей информации, доступный комментаторам, и могут также редактировать содержание статей.
- Владельцы могут редактировать любую часть истории, а также контролировать доступ других пользователей.
Структура данных
Предположим, в вашем приложении есть коллекция stories , где каждый документ представляет собой отдельную историю. У каждой истории также есть подколлекция comments , где каждый документ является комментарием к этой истории.
Для отслеживания ролей доступа добавьте поле roles , представляющее собой сопоставление идентификаторов пользователей с ролями:
/stories/{storyid}
{
title: "A Great Story",
content: "Once upon a time ...",
roles: {
alice: "owner",
bob: "reader",
david: "writer",
jane: "commenter"
// ...
}
}
Комментарии содержат всего два поля: идентификатор пользователя автора и некоторое содержимое:
/stories/{storyid}/comments/{commentid}
{
user: "alice",
content: "I think this is a great story!"
}
Правила
Now that you have users' roles recorded in the database, you need to write Security Rules to validate them. These rules assume the app uses Firebase Auth so that the request.auth.uid variable is the user's ID.
Шаг 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']);
}
}
}
}
Step 4 : Allow story writers, commenters, and owners to post comments. Note that this rule also validates that the owner of the comment matches the requesting user, which prevents users from writing over each other's comments:
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;
}
}
}
}
Step 5 : Give writers the ability to edit story content, but not to edit story roles or change any other properties of the document. This requires splitting the stories write rule into separate rules for create , update , and delete since writers can only update stories:
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;
}
}
}
}
Ограничения
Представленное выше решение демонстрирует защиту пользовательских данных с помощью правил безопасности, однако следует учитывать следующие ограничения:
- Granularity : In the example above, multiple roles (writer and owner) have write access to the same document but with different limitations. This can become hard to manage with more complex documents and it may be better to split single documents into multiple documents each owned by a single role.
- Большие группы : Если вам необходимо обмениваться данными с очень большими или сложными группами, рассмотрите систему, в которой роли хранятся в отдельной коллекции, а не в качестве поля в целевом документе.