סקירה כללית על התחביר הבסיסי של שפת כללי האבטחה של Realtime Database

כללי האבטחה של מסד הנתונים בזמן אמת ב-Firebase מאפשרים לכם לקבוע מי יוכל לגשת לנתונים שמאוחסנים במאגר הנתונים שלכם. תחביר הכללים הגמיש מאפשר ליצור כללים שתואמים לכל דבר, החל מכל הכתיבות למסד הנתונים ועד לפעולות בצמתים בודדים.

כללי האבטחה של Realtime Database הם הגדרה הצהרתית של מסד הנתונים. כלומר, הכללים מוגדרים בנפרד מהלוגיקה של המוצר. לגישה הזו יש כמה יתרונות: הלקוחות לא אחראים לאכיפת האבטחה, הטמעות עם באגים לא יסכנו את הנתונים, ואולי הכי חשוב – אין צורך בגורם מתווך, כמו שרת, כדי להגן על הנתונים מפני העולם.

בנושא הזה מתוארים התחביר והמבנה הבסיסיים של כללי האבטחה של מסד נתונים בזמן אמת, שמשמשים ליצירת קבוצות כללים מלאות.

הגדרת המבנה של כללי האבטחה

כללי האבטחה של מסד נתונים בזמן אמת מורכבים מביטויים דמויי JavaScript שכלולים במסמך JSON. מבנה הכללים צריך להיות תואם למבנה הנתונים שמאוחסנים במסד הנתונים.

כללים בסיסיים מזהים קבוצה של צמתים שצריך לאבטח, את שיטות הגישה (למשל, קריאה, כתיבה) שמעורבות, ואת התנאים שבהם הגישה מותרת או נדחית. בדוגמאות הבאות, התנאים יהיו הצהרות פשוטות של true ו-false, אבל בנושא הבא נסביר על דרכים דינמיות יותר לביטוי תנאים.

לדוגמה, אם אנחנו מנסים לאבטח child_node בתוך parent_node, התחביר הכללי הוא:

{
  "rules": {
    "parent_node": {
      "child_node": {
        ".read": <condition>,
        ".write": <condition>,
        ".validate": <condition>,
      }
    }
  }
}

בואו נשתמש בתבנית הזו. לדוגמה, נניח שאתם עוקבים אחרי רשימה של הודעות ויש לכם נתונים שנראים כך:

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

הכללים שלכם צריכים להיות בנויים בצורה דומה. הנה קבוצה של כללים לאבטחה לקריאה בלבד שעשויים להתאים למבנה הנתונים הזה. הדוגמה הזו ממחישה איך אנחנו מציינים צומתי מסד נתונים שעליהם חלים כללים, ואת התנאים להערכת כללים בצמתים האלה.

{
  "rules": {
    // For requests to access the 'messages' node...
    "messages": {
      // ...and the individual wildcarded 'message' nodes beneath
      // (we'll cover wildcarding variables more a bit later)....
      "$message": {

        // For each message, allow a read operation if <condition>. In this
        // case, we specify our condition as "true", so read access is always granted.
        ".read": "true",

        // For read-only behavior, we specify that for write operations, our
        // condition is false.
        ".write": "false"
      }
    }
  }
}

פעולות בסיסיות בכללים

יש שלושה סוגים של כללים לאכיפת אבטחה, בהתאם לסוג הפעולה שמבוצעת על הנתונים: .write,‏ .read ו-.validate. הנה סיכום קצר של המטרות שלהם:

סוגי כללים
‎.read מתאר אם ומתי משתמשים יכולים לקרוא נתונים.
‎.write מתאר אם ומתי מותר לכתוב נתונים.
‎.validate מגדיר איך נראה ערך בפורמט תקין, אם יש לו מאפייני צאצא ומה סוג הנתונים.

משתנים של תיעוד תווים כלליים לחיפוש

כל ההצהרות של הכללים מצביעות על צמתים. הצהרה יכולה להצביע על צומת ספציפי או להשתמש ב$ משתני תיעוד של תווים כלליים לחיפוש כדי להצביע על קבוצות של צמתים ברמה מסוימת בהיררכיה. אפשר להשתמש במשתני תיעוד כדי לאחסן את הערך של מפתחות הצומת לשימוש בהצהרות של כללים עוקבים. השיטה הזו מאפשרת לכתוב תנאים מורכבים יותר ב-Security Rules, ונסביר עליהם בפירוט בנושא הבא.

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

אפשר גם להשתמש במשתנים הדינמיים של $ במקביל לשמות נתיבים קבועים. בדוגמה הזו, אנחנו משתמשים במשתנה $other כדי להצהיר על כלל .validate שמוודא של-widget אין צאצאים מלבד title ו-color. כל פעולת כתיבה שתוביל ליצירת צאצאים נוספים תיכשל.

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}

כללי קריאה וכתיבה מדורגים

הכללים של .read ו-.write פועלים מלמעלה למטה, וכללים ברמה גבוהה יותר מבטלים כללים ברמה נמוכה יותר. אם כלל מעניק הרשאות קריאה או כתיבה בנתיב מסוים, הוא גם מעניק גישה לכל צומתי הצאצאים שמתחתיו. לדוגמה, במבנה הבא:

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          /* ignored, since read was allowed already */
          ".read": false
        }
     }
  }
}

מבנה האבטחה הזה מאפשר לקרוא מ-/bar/ בכל פעם ש-/foo/ מכיל צאצא baz עם הערך true. לכלל ".read": false ב-/foo/bar/ אין השפעה כאן, כי אי אפשר לבטל את הגישה באמצעות נתיב צאצא.

אולי זה לא נראה אינטואיטיבי בהתחלה, אבל זה חלק חשוב משפת הכללים, והוא מאפשר להטמיע הרשאות גישה מורכבות מאוד במינימום מאמץ. נדגים את זה בהמשך המדריך, כשנדון באבטחה מבוססת-משתמש.

חשוב לדעת שהכללים של .validate לא עוברים בירושה. כדי לאפשר כתיבה, כל כללי האימות צריכים להתקיים בכל הרמות של ההיררכיה.

כללים הם לא מסננים

הכללים מוחלים באופן אטומי. כלומר, פעולת קריאה או כתיבה נכשלת באופן מיידי אם אין כלל במיקום הזה או במיקום הורה שמעניק גישה. גם אם כל נתיב צאצא שמושפע נגיש, הקריאה במיקום ההורה תיכשל לחלוטין. לדוגמה:

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

אם לא מבינים שהכללים נבדקים באופן אטומי, יכול להיות שיהיה נראה שאחזור הנתיב /records/ יחזיר את rec1 אבל לא את rec2. אבל התוצאה בפועל היא שגיאה:

JavaScript
var db = firebase.database();
db.ref("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Objective-C
הערה: מוצר Firebase הזה לא זמין ביעד של App Clip.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  // success block is not called
} withCancelBlock:^(NSError * _Nonnull error) {
  // cancel block triggered with PERMISSION_DENIED
}];
Swift
הערה: מוצר Firebase הזה לא זמין ביעד של App Clip.
var ref = FIRDatabase.database().reference()
ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

מכיוון שפעולת הקריאה ב-/records/ היא אטומית, ואין כלל קריאה שמעניק גישה לכל הנתונים ב-/records/, הפעולה הזו תגרום לשגיאת PERMISSION_DENIED. אם נבדוק את הכלל הזה בסימולטור האבטחה במסוף Firebase, נראה שפעולת הקריאה נדחתה כי אין כלל קריאה שמאפשר גישה לנתיב /records/. עם זאת, חשוב לשים לב שהכלל של rec1 לא נבדק כי הוא לא היה בנתיב שביקשנו. כדי לאחזר את rec1, אנחנו צריכים לגשת אליו ישירות:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
הערה: מוצר Firebase הזה לא זמין ביעד של App Clip.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
הערה: מוצר Firebase הזה לא זמין ביעד של App Clip.
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records/rec1");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

הצהרות חופפות

יכול להיות שיותר מכלל אחד יחול על צומת. אם כמה ביטויים של כללים מזהים צומת, שיטת הגישה נדחית אם אחד מהתנאים הוא false:

{
  "rules": {
    "messages": {
      // A rule expression that applies to all nodes in the 'messages' node
      "$message": {
        ".read": "true",
        ".write": "true"
      },
      // A second rule expression applying specifically to the 'message1` node
      "message1": {
        ".read": "false",
        ".write": "false"
      }
    }
  }
}

בדוגמה שלמעלה, פעולות קריאה לצומת message1 יידחו כי הכלל השני הוא תמיד false, למרות שהכלל הראשון הוא תמיד true.

השלבים הבאים

כדי להבין טוב יותר את כללי האבטחה של מסד הנתונים בזמן אמת ב-Firebase, אתם יכולים:

  • המושג החשוב הבא בשפת Security Rules הוא תנאים דינמיים, שמאפשרים ל-Security Rules לבדוק את הרשאת המשתמש, להשוות בין נתונים קיימים לנתונים נכנסים, לאמת נתונים נכנסים, לבדוק את מבנה השאילתות שמגיעות מהלקוח ועוד.

  • כדאי לעיין בתרחישים נפוצים לדוגמה בתחום האבטחה ובהגדרות של כללי האבטחה של Firebase שמתייחסות אליהם.