אפליקציות רבות לעבודה משותפת מאפשרות למשתמשים לקרוא ולכתוב קטעי נתונים שונים על סמך קבוצת הרשאות. לדוגמה, באפליקציה לעריכת מסמכים, המשתמשים יכולים לאפשר לכמה משתמשים לקרוא ולכתוב את המסמכים שלהם, תוך חסימת גישה לא רצויה.
פתרון: בקרת גישה מבוססת-תפקיד
אתם יכולים להשתמש במודל הנתונים של 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
הוא המזהה של המשתמש.
שלב 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']);
}
}
}
}
שלב 4: מאפשרים לכותבי הסטוריז, למגיבים ולבעלים של הסטוריז לפרסם תגובות.
שימו לב שהכלל הזה מאמת גם שהערך של 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;
}
}
}
}
שלב 5: נותנים לכותבים אפשרות לערוך את תוכן הסיפור, אבל לא לערוך את התפקידים בסיפור או לשנות מאפיינים אחרים של המסמך. לשם כך, צריך לפצל את הכלל 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;
}
}
}
}
מגבלות
הפתרון שמוצג למעלה מראה איך לאבטח את נתוני המשתמשים באמצעות כללי אבטחה, אבל חשוב לדעת על המגבלות הבאות:
- רמת פירוט: בדוגמה שלמעלה, למספר תפקידים (כותב ובעלים) יש הרשאת כתיבה לאותו מסמך, אבל עם מגבלות שונות. יכול להיות שיהיה קשה לנהל את זה במסמכים מורכבים יותר, ולכן כדאי לפצל מסמכים בודדים למספר מסמכים, שבכל אחד מהם יש תפקיד אחד.
- קבוצות גדולות: אם אתם צריכים לשתף עם קבוצות גדולות או מורכבות מאוד, כדאי לשקול מערכת שבה התפקידים מאוחסנים באוסף משלהם ולא כשדה במסמך היעד.