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

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

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

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

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

כללי האבטחה של 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 המאפיין הזה מגדיר איך ייראה ערך בפורמט תקין, אם יש לו מאפייני צאצא ואת סוג הנתונים.

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

כל הצהרות הכללים מפנות לצמתים. ההצהרה יכולה להפנות לצומת ספציפי או להשתמש ב$ משתני לכידה של תווים כלליים לחיפוש כדי להפנות לקבוצות של צמתים ברמה מסוימת בהיררכיה. אפשר להשתמש במשתני הלכידה האלה כדי לאחסן את הערך של מפתחות הצמתים לשימוש בהצהרות כללים עוקבות. הטכניקה הזו מאפשרת לכתוב 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 Realtime Database:

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

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