بسیاری از برنامههای مشارکتی به کاربران اجازه میدهند تا براساس مجموعهای از اجازهها، بخشهای مختلف داده را بخوانند و بنویسند. برای مثال، در برنامه ویرایش سند، کاربران ممکن است بخواهند به چند کاربر اجازه دهند اسنادشان را بخوانند و بنویسند و درعینحال دسترسیهای ناخواسته را مسدود کنند.
راهحل: کنترل دسترسی نقشمبنا
میتوانید از مدل داده 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!"
}
قوانین
اکنون که نقشهای کاربران را در پایگاه داده ثبت کردهاید، باید «قوانین امنیتی» را برای اعتبارسنجی آنها بنویسید. این قوانین فرض میکنند که برنامه از
Firebase Auth استفاده میکند تا متغیر request.auth.uid
شناسه کاربر باشد.
مرحله ۱: با یک فایل قوانین پایه شروع کنید که شامل قوانین خالی برای داستانها و نظرات است:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{story} {
// TODO: Story rules go here...
match /comments/{comment} {
// TODO: Comment rules go here...
}
}
}
}
مرحله ۲: یک قانون ساده 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} {
// ...
}
}
}
}
مرحله ۳: قوانینی بنویسید که به کاربر با هر نقشی اجازه دهد داستانها و نظرات را بخواند. استفاده از توابع تعریفشده در مرحله قبلی باعث میشود قوانین مختصر و خوانا باقی بمانند:
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']);
}
}
}
}
مرحله ۴: به نویسندگان، نظردهندگان، و مالکان داستان اجازه دهید نظر پست کنند.
توجه داشته باشید که این قانون همچنین تأیید میکند که 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;
}
}
}
}
مرحله ۵: به نویسندگان امکان دهید محتوای داستان را ویرایش کنند، اما نتوانند نقشهای داستان را ویرایش کنند یا ویژگیهای دیگر سند را تغییر دهند. برای این کار باید قانون داستانهای write به قوانین جداگانهای برای create، update، و 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;
}
}
}
}
محدودیتها
راهکار نشاندادهشده در بالا نحوه ایمنسازی دادههای کاربر را بااستفاده از «قوانین امنیتی» نشان میدهد، اما باید از محدودیتهای زیر آگاه باشید:
- دقت: در مثال بالا، چندین نقش (نویسنده و مالک) دسترسی نوشتن به سند یکسانی دارند اما با محدودیتهای متفاوت. مدیریت این کار با اسناد پیچیدهتر میتواند دشوار شود و شاید بهتر باشد اسناد تکی را به چند سند تقسیم کنید که هرکدام متعلق به یک نقش باشد.
- گروههای بزرگ: اگر لازم است با گروههای بسیار بزرگ یا پیچیده همرسانی کنید، سیستمی را درنظر بگیرید که نقشها در آن بهجای اینکه بهعنوان فیلدی در سند هدف ذخیره شوند، در مجموعه خودشان ذخیره شوند.