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

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

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

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

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

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

שימו לב: כללי .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 הזה לא זמין ביעד של קטע מקדים לאפליקציה.
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 הזה לא זמין ביעד 'קליפ של אפליקציה'.
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 הזה לא זמין ביעד של קטע מקדים לאפליקציה.
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
הערה: מוצר Firebase הזה לא זמין ביעד 'קליפ של אפליקציה'.
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:

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

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