Acesso seguro aos dados para utilizadores e grupos
Muitas apps colaborativas permitem que os utilizadores leiam e escrevam diferentes partes de dados com base num conjunto de autorizações. Por exemplo, numa app de edição de documentos, os utilizadores podem querer permitir que alguns utilizadores leiam e escrevam os seus documentos, ao mesmo tempo que bloqueiam o acesso indesejado.
Solução: controlo de acesso baseado em funções
Pode tirar partido do modelo de dados do Cloud Firestore, bem como das regras de segurança personalizadas para implementar o controlo de acesso baseado em funções na sua app.
Suponhamos que está a criar uma aplicação de escrita colaborativa na qual os utilizadores podem criar "histórias" e "comentários" com os seguintes requisitos de segurança:
- Cada história tem um proprietário e pode ser partilhada com "escritores", "comentadores" e "leitores".
- Os leitores só podem ver histórias e comentários. Não podem editar nada.
- Os comentadores têm todo o acesso dos leitores e também podem adicionar comentários a uma história.
- Os escritores têm todo o acesso dos comentadores e também podem editar o conteúdo das histórias.
- Os proprietários podem editar qualquer parte de uma história, bem como controlar o acesso de outros utilizadores.
Estrutura de dados
Suponha que a sua app tem uma coleção stories
em que cada documento representa uma história. Cada história também tem uma subcoleção comments
onde cada documento
é um comentário sobre essa história.
Para acompanhar as funções de acesso, adicione um campo roles
, que é um mapeamento de IDs dos utilizadores para funções:
/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 do utilizador do autor e algum conteúdo:
/stories/{storyid}/comments/{commentid}
{
user: "alice",
content: "I think this is a great story!"
}
Regras
Agora que tem as funções dos utilizadores registadas na base de dados, tem de escrever regras de segurança para as validar. Estas regras pressupõem que a app usa o
Firebase Auth para que a variável request.auth.uid
seja o ID do utilizador.
Passo 1: comece com um ficheiro de regras básico, que inclui 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...
}
}
}
}
Passo 2: adicione uma regra simples write
que dê aos proprietários controlo total sobre as histórias. As funções definidas ajudam a determinar as funções de um utilizador e se os 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 um utilizador de qualquer função ler histórias e comentários. A utilização das funções definidas no passo 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']);
}
}
}
}
Passo 4: permita que os autores, os comentadores e os proprietários das histórias publiquem comentários.
Tenha em atenção que esta regra também valida se o owner
do comentário corresponde ao utilizador que está a fazer o pedido, o que impede que os utilizadores substituam os comentários uns dos outros:
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;
}
}
}
}
Passo 5: conceda aos escritores a capacidade de editar o conteúdo das notícias, mas não de editar as funções das notícias nem alterar quaisquer outras propriedades do documento. Isto requer a divisão da regra de write
histórias em regras separadas para create
, update
e delete
, uma vez que 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 apresentada acima demonstra a proteção dos dados do utilizador através das regras de segurança, mas deve ter em atenção as seguintes limitações:
- Nível de detalhe: no exemplo acima, várias funções (autor e proprietário) têm acesso de escrita ao mesmo documento, mas com limitações diferentes. Isto pode tornar-se difícil de gerir com documentos mais complexos e pode ser melhor dividir documentos únicos em vários documentos, cada um pertencente a uma única função.
- Grupos grandes: se precisar de partilhar com grupos muito grandes ou complexos, considere um sistema em que as funções são armazenadas na sua própria coleção, em vez de como um campo no documento de destino.