Muitos apps colaborativos permitem que os usuários leiam e escrevam diferentes partes de dados com base em um conjunto de permissões. Em um app de edição de documentos, por exemplo, talvez os usuários queiram permitir que algumas pessoas leiam e escrevam os documentos deles ao mesmo tempo que bloqueiam os acessos indesejados.
Solução: controle de acesso com base em papéis
Aproveite o modelo de dados do Cloud Firestore e as regras de segurança personalizadas para implementar controles de acesso com base em papéis no seu app.
Imagine que você esteja criando um aplicativo colaborativo de escrita. Nele, os usuários podem criar "histórias" e "comentários" com os seguintes requisitos de segurança:
- Cada história tem um proprietário e pode ser compartilhada com "escritores", "comentaristas" e "leitores".
- Os leitores só podem ver as histórias e os comentários. Eles não podem editar nada.
- Os comentaristas têm o mesmo nível de acesso que os leitores, mas eles também podem adicionar comentários a uma história.
- Os escritores têm o mesmo nível de acesso que os comentaristas, mas eles também podem editar o conteúdo da história.
- Os proprietários podem editar qualquer parte de uma história e também controlar o acesso de outros usuários.
Estrutura de dados
Suponha que seu aplicativo tenha uma coleção stories
em que os documentos representam
histórias. Cada história também tem uma subcoleção comments
em que cada documento
é um comentário sobre a história.
Para acompanhar os papéis de acesso, adicione um campo roles
que é um mapa de
IDs de usuário aos papéis:
/stories/{storyid}
{
title: "A Great Story",
content: "Once upon a time ...",
roles: {
alice: "owner",
bob: "reader",
david: "writer",
jane: "commenter"
// ...
}
}
Os comentários contêm apenas dois campos, o ID de usuário do autor e algum conteúdo:
/stories/{storyid}/comments/{commentid}
{
user: "alice",
content: "I think this is a great story!"
}
Regras
Agora que os papéis dos usuários estão gravados no banco de dados, você precisa escrever
regras de segurança para validá-los. Nessas regras, presume-se que o aplicativo use o Firebase Auth para que a variável request.auth.uid
seja o ID do usuário.
Etapa 1: comece com um arquivo de regras básicas, que inclua regras vazias para histórias e comentários:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
// TODO: Story rules go here...
match /comments/{comment} {
// TODO: Comment rules go here...
}
}
}
}
Etapa 2: adicione uma regra write
simples que dê aos proprietários controle total sobre as histórias. As funções definidas ajudam a determinar os papéis de um usuário e se novos
documentos são válidos:
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} {
// ...
}
}
}
}
Passo 3: escreva regras que permitam a usuários de qualquer papel a leitura de histórias e comentários. O uso das funções definidas na etapa anterior mantém as regras concisas e legíveis:
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']);
}
}
}
}
Etapa 4: permita a postagem de comentários por parte dos escritores, comentaristas e proprietários.
Observe que essa regra também valida se o owner
do comentário corresponde ao usuário solicitante,
o que impede que os usuários escrevam sobre os comentários um do outro:
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;
}
}
}
}
Etapa 5: permita que os escritores editem o conteúdo da história,
mas não os papéis dela ou outras propriedades do documento. Isso exige a divisão da regra
write
de histórias em regras separadas para create
, update
e
delete
, porque os escritores só podem atualizar histórias:
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;
}
}
}
}
Limitações
A solução indicada acima demonstra como proteger os dados do usuário com regras de segurança. Porém, ela está sujeita às seguintes limitações:
- Granularidade: no exemplo acima, usuários com vários papéis (escritor e proprietário) têm acesso de escrita ao mesmo documento, mas com limitações diferentes. O gerenciamento dessa situação pode se tornar difícil no caso de documentos mais complexos. Talvez seja melhor dividir documentos únicos em vários, cada um deles pertencendo a um único papel.
- Grupos grandes: se você precisa compartilhar os dados com grupos muito grandes ou complexos, é recomendável usar um sistema em que os papéis sejam armazenados na própria coleção e não como um campo no documento de destino.