Firebase के सुरक्षा नियमों की मदद से अपने Firestore डेटा को सुरक्षित रखें

1. शुरू करने से पहले

Cloud Firestore, Cloud Storage for Firebase, और Realtime Database, कॉन्फ़िगरेशन फ़ाइलों पर निर्भर करते हैं. इन फ़ाइलों को लिखकर, पढ़ने और लिखने का ऐक्सेस दिया जाता है. इस कॉन्फ़िगरेशन को सुरक्षा के नियम कहा जाता है. यह आपके ऐप्लिकेशन के लिए एक तरह के स्कीमा के तौर पर भी काम कर सकता है. यह आपके ऐप्लिकेशन को डेवलप करने का सबसे अहम हिस्सा है. इस कोडलैब में, आपको इसके बारे में जानकारी मिलेगी.

ज़रूरी शर्तें

  • Visual Studio Code, Atom या Sublime Text जैसे सामान्य एडिटर
  • Node.js 8.6.0 या इसके बाद का वर्शन (Node.js इंस्टॉल करने के लिए, nvm का इस्तेमाल करें; अपना वर्शन देखने के लिए, node --version चलाएं)
  • Java 7 या इसके बाद का वर्शन (Java इंस्टॉल करने के लिए, इन निर्देशों का पालन करें; अपने वर्शन की जांच करने के लिए, java -version चलाएं)

आपको क्या करना होगा

इस कोडलैब में, Firestore पर बनाए गए एक सामान्य ब्लॉग प्लैटफ़ॉर्म को सुरक्षित किया जाएगा. आपको Security Rules के ख़िलाफ़ यूनिट टेस्ट चलाने के लिए, Firestore एम्युलेटर का इस्तेमाल करना होगा. साथ ही, यह पक्का करना होगा कि नियमों के तहत, आपको उम्मीद के मुताबिक ऐक्सेस मिले और न मिले.

आपको, इनके बारे में जानकारी मिलेगी:

  • अनुमति को कंट्रोल करने की बेहतर सुविधा
  • डेटा और टाइप की पुष्टि करने की सुविधा लागू करना
  • एट्रिब्यूट के आधार पर ऐक्सेस कंट्रोल लागू करना
  • पुष्टि करने के तरीके के आधार पर ऐक्सेस देना
  • कस्टम फ़ंक्शन बनाना
  • समय के हिसाब से सुरक्षा के नियम बनाना
  • अनुमति न दी गई सूची और कुछ समय के लिए मिटाए गए आइटम लागू करना
  • यह समझना कि कई ऐक्सेस पैटर्न को पूरा करने के लिए, डेटा को कब डीनॉर्मलाइज़ करना है

2. सेट अप करें

यह एक ब्लॉगिंग ऐप्लिकेशन है. यहां ऐप्लिकेशन की सुविधाओं के बारे में खास जानकारी दी गई है:

ब्लॉग पोस्ट के ड्राफ़्ट:

  • उपयोगकर्ता, ब्लॉग पोस्ट के ड्राफ़्ट बना सकते हैं. ये ड्राफ़्ट, drafts कलेक्शन में सेव होते हैं.
  • लेखक, ड्राफ़्ट को तब तक अपडेट कर सकता है, जब तक वह पब्लिश करने के लिए तैयार न हो जाए.
  • पब्लिश करने के लिए तैयार होने पर, Firebase फ़ंक्शन ट्रिगर होता है. यह published कलेक्शन में एक नया दस्तावेज़ बनाता है.
  • ड्राफ़्ट को लेखक या साइट के मॉडरेटर मिटा सकते हैं

पब्लिश की गई ब्लॉग पोस्ट:

  • पब्लिश की गई पोस्ट, उपयोगकर्ता नहीं बना सकते. इन्हें सिर्फ़ किसी फ़ंक्शन के ज़रिए बनाया जा सकता है.
  • इन्हें सिर्फ़ कुछ समय के लिए मिटाया जा सकता है. इससे visible एट्रिब्यूट की वैल्यू बदलकर false हो जाती है.

टिप्पणियां

  • पब्लिश की गई पोस्ट पर टिप्पणियां की जा सकती हैं. ये टिप्पणियां, पब्लिश की गई हर पोस्ट पर एक सब-कलेक्शन के तौर पर दिखती हैं.
  • गलत इस्तेमाल को कम करने के लिए, उपयोगकर्ताओं के पास पुष्टि किया गया ईमेल पता होना चाहिए. साथ ही, टिप्पणी करने के लिए, उन्हें 'तथ्यों को खारिज करने वाले' के तौर पर मार्क नहीं किया गया होना चाहिए.
  • टिप्पणी पोस्ट करने के एक घंटे के अंदर ही उसे अपडेट किया जा सकता है.
  • टिप्पणियों को, टिप्पणी करने वाला व्यक्ति, मूल पोस्ट का लेखक या मॉडरेटर मिटा सकते हैं.

ऐक्सेस के नियमों के साथ-साथ, आपको सुरक्षा के नियम भी बनाने होंगे. ये नियम, ज़रूरी फ़ील्ड और डेटा की पुष्टि करने की सुविधा को लागू करते हैं.

Firebase Emulator Suite का इस्तेमाल करके, सभी कार्रवाइयां स्थानीय तौर पर की जाएंगी.

सोर्स कोड पाना

इस कोडलैब में, आपको सुरक्षा नियमों के लिए टेस्ट से शुरुआत करनी होगी. हालांकि, इसमें कम से कम सुरक्षा नियम शामिल होंगे. इसलिए, टेस्ट चलाने के लिए आपको सबसे पहले सोर्स को क्लोन करना होगा:

$ git clone https://github.com/FirebaseExtended/codelab-rules.git

इसके बाद, initial-state डायरेक्ट्री में जाएं. इस कोडलैब का बाकी काम यहीं किया जाएगा:

$ cd codelab-rules/initial-state

अब, डिपेंडेंसी इंस्टॉल करें, ताकि टेस्ट चलाए जा सकें. अगर आपके इंटरनेट कनेक्शन की स्पीड धीमी है, तो इसमें एक या दो मिनट लग सकते हैं:

# Move into the functions directory, install dependencies, jump out.
$ cd functions && npm install && cd -

Firebase CLI इंस्टॉल करना

टेस्ट चलाने के लिए इस्तेमाल किया जाने वाला Emulator Suite, Firebase CLI (कमांड-लाइन इंटरफ़ेस) का हिस्सा है. इसे अपनी मशीन पर इंस्टॉल करने के लिए, यह कमांड इस्तेमाल करें:

$ npm install -g firebase-tools

इसके बाद, पुष्टि करें कि आपके पास सीएलआई का नया वर्शन है. यह कोडलैब, 8.4.0 या इसके बाद के वर्शन के साथ काम करना चाहिए. हालांकि, बाद के वर्शन में गड़बड़ियों को ठीक करने से जुड़ी ज़्यादा सुविधाएं शामिल हैं.

$ firebase --version
9.10.2

3. टेस्ट चलाना

इस सेक्शन में, आपको स्थानीय तौर पर टेस्ट चलाने के बारे में जानकारी मिलेगी. इसका मतलब है कि अब Emulator Suite को बूट अप करने का समय आ गया है.

एम्युलेटर शुरू करना

आपको जिस ऐप्लिकेशन के साथ काम करना है उसमें तीन मुख्य Firestore कलेक्शन हैं: drafts में ऐसी ब्लॉग पोस्ट शामिल हैं जिन पर काम चल रहा है, published कलेक्शन में वे ब्लॉग पोस्ट शामिल हैं जिन्हें पब्लिश किया जा चुका है, और comments पब्लिश की गई पोस्ट का सबकलेक्शन है. इस रेपो में, सुरक्षा नियमों के लिए यूनिट टेस्ट शामिल हैं. ये नियम, उपयोगकर्ता के एट्रिब्यूट और अन्य ज़रूरी शर्तों के बारे में बताते हैं. इन शर्तों को पूरा करने पर ही उपयोगकर्ता, drafts, published, और comments कलेक्शन में दस्तावेज़ बना सकता है, उन्हें पढ़ सकता है, अपडेट कर सकता है, और मिटा सकता है. आपको सुरक्षा के नियम लिखने होंगे, ताकि वे टेस्ट पास हो सकें.

शुरुआत में, आपका डेटाबेस लॉक हो जाता है. इसका मतलब है कि डेटाबेस को पढ़ने और लिखने की अनुमति किसी को नहीं होती. साथ ही, सभी टेस्ट फ़ेल हो जाते हैं. सुरक्षा के नियम लिखते समय, जांचें पास हो जाएंगी. टेस्ट देखने के लिए, अपने एडिटर में functions/test.js खोलें.

कमांड लाइन पर, emulators:exec का इस्तेमाल करके एम्युलेटर शुरू करें और टेस्ट चलाएं:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"

स्क्रोल करके आउटपुट के सबसे ऊपर जाएं:

$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
i  emulators: Starting emulators: functions, firestore, hosting
⚠  functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub
⚠  functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.
i  firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata
i  firestore: Firestore Emulator logging to firestore-debug.log
⚠  hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login?
⚠  hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase.
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000
i  functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions...
✔  functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost).
✔  functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete).
i  Running script: pushd functions; npm test
~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state

> functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions
> mocha

(node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time


  Draft blog posts
    1) can be created with required fields by the author
    2) can be updated by author if immutable fields are unchanged
    3) can be read by the author and moderator

  Published blog posts
    4) can be read by everyone; created or deleted by no one
    5) can be updated by author or moderator

  Comments on published blog posts
    6) can be read by anyone with a permanent account
    7) can be created if email is verfied and not blocked
    8) can be updated by author for 1 hour after creation
    9) can be deleted by an author or moderator


  0 passing (848ms)
  9 failing

...

फ़िलहाल, नौ अनुरोध पूरे नहीं किए जा सके. नियमों की फ़ाइल बनाते समय, ज़्यादा टेस्ट पास होने पर प्रोग्रेस का पता लगाया जा सकता है.

4. ब्लॉग पोस्ट के ड्राफ़्ट बनाना.

ड्राफ़्ट की गई ब्लॉग पोस्ट का ऐक्सेस, पब्लिश की गई ब्लॉग पोस्ट के ऐक्सेस से अलग होता है. इसलिए, यह ब्लॉगिंग ऐप्लिकेशन, ड्राफ़्ट की गई ब्लॉग पोस्ट को एक अलग कलेक्शन, /drafts में सेव करता है. ड्राफ़्ट को सिर्फ़ लेखक या मॉडरेटर ऐक्सेस कर सकता है. साथ ही, इसमें ज़रूरी और न बदले जा सकने वाले फ़ील्ड के लिए पुष्टि करने की सुविधा होती है.

firestore.rules फ़ाइल खोलने पर, आपको डिफ़ॉल्ट नियमों वाली फ़ाइल दिखेगी:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

मैच स्टेटमेंट, match /{document=**}, ** सिंटैक्स का इस्तेमाल कर रहा है, ताकि सब-कलेक्शन के सभी दस्तावेज़ों पर इसे बार-बार लागू किया जा सके. यह सबसे ऊपर वाले लेवल पर है. इसलिए, फ़िलहाल सभी अनुरोधों पर एक ही नियम लागू होता है. इससे कोई फ़र्क़ नहीं पड़ता कि अनुरोध कौन कर रहा है या वह किस डेटा को पढ़ने या लिखने की कोशिश कर रहा है.

सबसे पहले, सबसे अंदरूनी मैच स्टेटमेंट को हटाएं और उसकी जगह match /drafts/{draftID} लिखें. (दस्तावेज़ों के स्ट्रक्चर के बारे में टिप्पणियां, नियमों को समझने में मददगार हो सकती हैं. इन्हें इस कोडलैब में शामिल किया जाएगा. हालांकि, ये हमेशा वैकल्पिक होती हैं.)

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional
    }
  }
}

ड्राफ़्ट के लिए बनाया गया पहला नियम यह तय करेगा कि दस्तावेज़ कौन बना सकता है. इस ऐप्लिकेशन में, ड्राफ़्ट सिर्फ़ लेखक के तौर पर शामिल व्यक्ति बना सकता है. देखें कि अनुरोध करने वाले व्यक्ति का यूआईडी, दस्तावेज़ में दिए गए यूआईडी से मेल खाता हो.

क्रिएट करने के लिए पहली शर्त यह होगी:

request.resource.data.authorUID == request.auth.uid

इसके बाद, दस्तावेज़ सिर्फ़ तब बनाए जा सकते हैं, जब उनमें तीन ज़रूरी फ़ील्ड शामिल हों: authorUID,createdAt, और title. (उपयोगकर्ता, createdAt फ़ील्ड की जानकारी नहीं देता है. इससे यह पता चलता है कि दस्तावेज़ बनाने से पहले, ऐप्लिकेशन को यह फ़ील्ड जोड़ना होगा.) आपको सिर्फ़ यह देखना है कि एट्रिब्यूट बनाए जा रहे हैं या नहीं. इसलिए, यह देखा जा सकता है कि request.resource में वे सभी कुंजियां मौजूद हैं या नहीं:

request.resource.data.keys().hasAll([
  "authorUID",
  "createdAt",
  "title"
])

ब्लॉग पोस्ट बनाने के लिए, यह ज़रूरी है कि टाइटल में 50 से ज़्यादा वर्ण न हों:

request.resource.data.title.size() < 50

इन सभी शर्तों का पूरा होना ज़रूरी है. इसलिए, इन्हें लॉजिकल AND ऑपरेटर && के साथ जोड़ें. पहला नियम यह बन जाता है:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

टर्मिनल में, टेस्ट फिर से चलाएं और पुष्टि करें कि पहला टेस्ट पास हो गया है.

5. ब्लॉग पोस्ट के ड्राफ़्ट अपडेट करें.

इसके बाद, जब लेखक अपनी ड्राफ़्ट की गई ब्लॉग पोस्ट को बेहतर बनाएंगे, तब वे ड्राफ़्ट किए गए दस्तावेज़ों में बदलाव करेंगे. उन शर्तों के लिए एक नियम बनाएं जिनके हिसाब से पोस्ट को अपडेट किया जा सकता है. सबसे पहले, सिर्फ़ लेखक अपने ड्राफ़्ट अपडेट कर सकता है. ध्यान दें कि यहां आपको पहले से लिखे गए यूआईडी की जांच करनी है,resource.data.authorUID:

resource.data.authorUID == request.auth.uid

अपडेट के लिए दूसरी ज़रूरी शर्त यह है कि दो एट्रिब्यूट, authorUID और createdAt की वैल्यू में बदलाव नहीं होना चाहिए:

request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
]);

आखिर में, टाइटल में 50 या इससे कम वर्ण होने चाहिए:

request.resource.data.title.size() < 50;

इन सभी शर्तों को पूरा करना ज़रूरी है. इसलिए, इन्हें && के साथ जोड़ें:

allow update: if
  // User is the author, and
  resource.data.authorUID == request.auth.uid &&
  // `authorUID` and `createdAt` are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "createdAt"
  ]) &&
  // Title must be < 50 characters long
  request.resource.data.title.size() < 50;

इसलिए, पूरे नियम इस तरह से बन जाते हैं:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;
    }
  }
}

टेस्ट फिर से चलाएं और पुष्टि करें कि कोई दूसरा टेस्ट पास हो गया है.

6. ड्राफ़्ट मिटाना और उन्हें पढ़ना: एट्रिब्यूट के आधार पर ऐक्सेस कंट्रोल

लेखक, ड्राफ़्ट बना सकते हैं और उन्हें अपडेट कर सकते हैं. साथ ही, वे ड्राफ़्ट मिटा भी सकते हैं.

resource.data.authorUID == request.auth.uid

इसके अलावा, जिन लेखकों के ऑथराइज़ेशन टोकन में isModerator एट्रिब्यूट मौजूद है उन्हें ड्राफ़्ट मिटाने की अनुमति है:

request.auth.token.isModerator == true

मिटाने के लिए, इनमें से कोई भी एक शर्त काफ़ी है. इसलिए, इन्हें लॉजिकल OR ऑपरेटर, || के साथ जोड़ें:

allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

पढ़ने की अनुमति के लिए भी यही शर्तें लागू होती हैं, ताकि अनुमति को नियम में जोड़ा जा सके:

allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true

अब पूरे नियम ये हैं:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }
  }
}

टेस्ट फिर से चलाएं और पुष्टि करें कि अब कोई दूसरा टेस्ट पास हो गया है.

7. पब्लिश की गई पोस्ट को पढ़ता है, बनाता है, और मिटाता है: अलग-अलग ऐक्सेस पैटर्न के लिए डीनॉर्मलाइज़ करना

पब्लिश की गई पोस्ट और ड्राफ़्ट पोस्ट के ऐक्सेस पैटर्न अलग-अलग होते हैं. इसलिए, यह ऐप्लिकेशन पोस्ट को अलग-अलग draft और published कलेक्शन में डीनॉर्मलाइज़ करता है. उदाहरण के लिए, पब्लिश की गई पोस्ट को कोई भी पढ़ सकता है. हालांकि, उन्हें पूरी तरह से नहीं मिटाया जा सकता. वहीं, ड्राफ़्ट को मिटाया जा सकता है. हालांकि, उन्हें सिर्फ़ लेखक और मॉडरेटर पढ़ सकते हैं. इस ऐप्लिकेशन में, जब कोई उपयोगकर्ता ड्राफ़्ट की गई ब्लॉग पोस्ट को पब्लिश करना चाहता है, तो एक फ़ंक्शन ट्रिगर होता है. यह फ़ंक्शन, पब्लिश की गई नई पोस्ट बनाता है.

इसके बाद, पब्लिश की गई पोस्ट के लिए नियम लिखें. पोस्ट लिखने के लिए सबसे आसान नियम यह है कि पब्लिश की गई पोस्ट को कोई भी पढ़ सकता है. हालांकि, उन्हें कोई भी न तो बना सकता है और न ही मिटा सकता है. ये नियम जोड़ें:

match /published/{postID} {
  // `authorUID`: string, required
  // `content`: string, required
  // `publishedAt`: timestamp, required
  // `title`: string, < 50 characters, required
  // `url`: string, required
  // `visible`: boolean, required

  // Can be read by everyone
  allow read: if true;

  // Published posts are created only via functions, never by users
  // No hard deletes; soft deletes update `visible` field.
  allow create, delete: if false;
}

इन्हें मौजूदा नियमों में जोड़ने पर, नियमों वाली पूरी फ़ाइल यह बन जाती है:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
        ]) &&
        // Title must be < 50 characters long
        request.resource.data.title.size() < 50;

      allow read, delete: if
        // User is draft author
        resource.data.authorUID == request.auth.uid ||
        // User is a moderator
        request.auth.token.isModerator == true;
    }

    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;
    }
  }
}

टेस्ट फिर से चलाएं और पुष्टि करें कि कोई दूसरा टेस्ट पास हो गया हो.

8. पब्लिश की गई पोस्ट अपडेट करना: कस्टम फ़ंक्शन और लोकल वैरिएबल

पब्लिश की गई पोस्ट को अपडेट करने के लिए, ये शर्तें पूरी होनी चाहिए:

  • इसे सिर्फ़ लेखक या मॉडरेटर ही कर सकता है.
  • इसमें सभी ज़रूरी फ़ील्ड शामिल होने चाहिए.

आपने लेखक या मॉडरेटर बनने की शर्तें पहले ही लिख दी हैं. इसलिए, आपके पास इन शर्तों को कॉपी करके चिपकाने का विकल्प है. हालांकि, समय के साथ इन शर्तों को पढ़ना और बनाए रखना मुश्किल हो सकता है. इसके बजाय, आपको एक कस्टम फ़ंक्शन बनाना होगा, जिसमें लेखक या मॉडरेटर होने की लॉजिक शामिल हो. इसके बाद, इसे कई शर्तों के हिसाब से कॉल किया जाएगा.

कस्टम फ़ंक्शन बनाना

ड्राफ़्ट के लिए मैच स्टेटमेंट के ऊपर, isAuthorOrModerator नाम का एक नया फ़ंक्शन बनाएं. यह फ़ंक्शन, पोस्ट के दस्तावेज़ (यह ड्राफ़्ट या पब्लिश की गई पोस्ट, दोनों के लिए काम करेगा) और उपयोगकर्ता के पुष्टि करने वाले ऑब्जेक्ट को आर्ग्युमेंट के तौर पर लेता है:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {

    }

    match /drafts/{postID} {
      allow create: ...
      allow update: ...
      ...
    }

    match /published/{postID} {
      allow read: ...
      allow create, delete: ...
    }
  }
}

लोकल वैरिएबल का इस्तेमाल करना

फ़ंक्शन के अंदर, isAuthor और isModerator वैरिएबल सेट करने के लिए, let कीवर्ड का इस्तेमाल करें. सभी फ़ंक्शन, return स्टेटमेंट के साथ खत्म होने चाहिए. हमारा फ़ंक्शन, एक बूलियन वैल्यू दिखाएगा. इससे पता चलेगा कि कोई वैरिएबल सही है या नहीं:

function isAuthorOrModerator(post, auth) {
  let isAuthor = auth.uid == post.authorUID;
  let isModerator = auth.token.isModerator == true;
  return isAuthor || isModerator;
}

फ़ंक्शन को कॉल करना

अब ड्राफ़्ट के लिए नियम को अपडेट करें, ताकि उस फ़ंक्शन को कॉल किया जा सके. साथ ही, यह ध्यान रखें कि resource.data को पहले आर्ग्युमेंट के तौर पर पास किया गया हो:

  // Draft blog posts
  match /drafts/{draftID} {
    ...
    // Can be deleted by author or moderator
    allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
  }

अब पब्लिश की गई पोस्ट को अपडेट करने के लिए, एक शर्त लिखी जा सकती है. इसमें नए फ़ंक्शन का भी इस्तेमाल किया जा सकता है:

allow update: if isAuthorOrModerator(resource.data, request.auth);

मान्यताओं को जोड़ना

पब्लिश की गई पोस्ट के कुछ फ़ील्ड में बदलाव नहीं किया जाना चाहिए. खास तौर पर, url, authorUID, और publishedAt फ़ील्ड में बदलाव नहीं किया जा सकता. अपडेट के बाद भी, अन्य दो फ़ील्ड title और content, और visible मौजूद होने चाहिए. पब्लिश की गई पोस्ट में अपडेट करने के लिए, इन ज़रूरी शर्तों को लागू करने के लिए शर्तें जोड़ें:

// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
  "authorUID",
  "publishedAt",
  "url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
  "content",
  "title",
  "visible"
])

खुद कोई कस्टम फ़ंक्शन बनाना

आखिर में, यह शर्त जोड़ें कि टाइटल में 50 से ज़्यादा वर्ण न हों. इस लॉजिक का दोबारा इस्तेमाल किया जा रहा है. इसलिए, इसे titleIsUnder50Chars नाम का नया फ़ंक्शन बनाकर इस्तेमाल किया जा सकता है. नए फ़ंक्शन की मदद से, पब्लिश की गई पोस्ट को अपडेट करने की शर्त यह हो जाती है:

allow update: if
  isAuthorOrModerator(resource.data, request.auth) &&
  // Immutable fields are unchanged
  request.resource.data.diff(resource.data).unchangedKeys().hasAll([
    "authorUID",
    "publishedAt",
    "url"
  ]) &&
  // Required fields are present
  request.resource.data.keys().hasAll([
    "content",
    "title",
    "visible"
  ]) &&
  titleIsUnder50Chars(request.resource.data);

नियमों वाली पूरी फ़ाइल यह है:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User creating document is draft author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and url fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is the author, and
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }
  }
}

जांच फिर से चलाएं. इस समय तक, आपके पास पांच पास किए गए टेस्ट और चार फ़ेल किए गए टेस्ट होने चाहिए.

9. टिप्पणियां: उप-संग्रह और साइन-इन की सुविधा देने वाली कंपनी की अनुमतियां

पब्लिश की गई पोस्ट पर टिप्पणियां की जा सकती हैं. ये टिप्पणियां, पब्लिश की गई पोस्ट (/published/{postID}/comments/{commentID}) की सब-कलेक्शन में सेव होती हैं. डिफ़ॉल्ट रूप से, किसी कलेक्शन के नियम सब-कलेक्शन पर लागू नहीं होते. आपको टिप्पणियों पर वे नियम लागू नहीं करने हैं जो पब्लिश की गई पोस्ट के पैरंट दस्तावेज़ पर लागू होते हैं. इसलिए, आपको अलग-अलग नियम बनाने होंगे.

टिप्पणियों को ऐक्सेस करने के लिए नियम लिखने के लिए, मैच स्टेटमेंट से शुरुआत करें:

match /published/{postID}/comments/{commentID} {
  // `authorUID`: string, required
  // `comment`: string, < 500 characters, required
  // `createdAt`: timestamp, required
  // `editedAt`: timestamp, optional

टिप्पणियां पढ़ना: पहचान छिपाकर नहीं पढ़ा जा सकता

इस ऐप्लिकेशन के लिए, सिर्फ़ वे उपयोगकर्ता टिप्पणियां पढ़ सकते हैं जिन्होंने स्थायी खाता बनाया है. वे उपयोगकर्ता टिप्पणियां नहीं पढ़ सकते जिन्होंने गुमनाम खाता बनाया है. उस नियम को लागू करने के लिए, हर auth.token ऑब्जेक्ट पर मौजूद sign_in_provider एट्रिब्यूट देखें:

allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";

अपने टेस्ट फिर से चलाएं और पुष्टि करें कि एक और टेस्ट पास हो गया है.

टिप्पणियां बनाना: अस्वीकार की गई सूची की जांच करना

टिप्पणी करने के लिए, ये तीन शर्तें पूरी होनी चाहिए:

  • उपयोगकर्ता के पास पुष्टि किया गया ईमेल पता होना चाहिए
  • टिप्पणी में 500 से कम वर्ण होने चाहिए और
  • वे प्रतिबंधित उपयोगकर्ताओं की सूची में शामिल नहीं होने चाहिए. यह सूची, Firestore में bannedUsers कलेक्शन में सेव होती है. इन शर्तों को एक-एक करके देखते हैं:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

टिप्पणियां करने के लिए, आखिरी नियम यह है:

allow create: if
  // User has verified email
  (request.auth.token.email_verified == true) &&
  // UID is not on bannedUsers list
  !(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

नियमों वाली पूरी फ़ाइल अब यह है:

For bottom of step 9
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
    }
  }
}

टेस्ट फिर से चलाएं और पक्का करें कि एक और टेस्ट पास हो गया हो.

10. टिप्पणियां अपडेट करना: समय के हिसाब से बनाए गए नियम

टिप्पणियों के लिए कारोबार से जुड़ा लॉजिक यह है कि टिप्पणी करने वाला व्यक्ति, टिप्पणी करने के एक घंटे के अंदर उसमें बदलाव कर सकता है. इसे लागू करने के लिए, createdAt टाइमस्टैंप का इस्तेमाल करें.

सबसे पहले, यह पुष्टि करने के लिए कि उपयोगकर्ता ही लेखक है:

request.auth.uid == resource.data.authorUID

इसके बाद, यह जानकारी कि टिप्पणी पिछले एक घंटे में की गई है:

(request.time - resource.data.createdAt) < duration.value(1, 'h');

इन्हें लॉजिकल AND ऑपरेटर के साथ जोड़ने पर, टिप्पणियों को अपडेट करने का नियम यह बन जाता है:

allow update: if
  // is author
  request.auth.uid == resource.data.authorUID &&
  // within an hour of comment creation
  (request.time - resource.data.createdAt) < duration.value(1, 'h');

टेस्ट फिर से चलाएं और पक्का करें कि एक और टेस्ट पास हो गया हो.

11. टिप्पणियां मिटाई जा रही हैं: पैरंट के मालिकाना हक की पुष्टि की जा रही है

टिप्पणियों को, टिप्पणी करने वाला व्यक्ति, मॉडरेटर या ब्लॉग पोस्ट का लेखक मिटा सकता है.

सबसे पहले, आपने पहले जो हेल्पर फ़ंक्शन जोड़ा था वह authorUID फ़ील्ड की जांच करता है. यह फ़ील्ड किसी पोस्ट या टिप्पणी पर मौजूद हो सकता है. इसलिए, हेल्पर फ़ंक्शन का फिर से इस्तेमाल करके यह जांच की जा सकती है कि उपयोगकर्ता लेखक या मॉडरेटर है या नहीं:

isAuthorOrModerator(resource.data, request.auth)

यह देखने के लिए कि उपयोगकर्ता, ब्लॉग पोस्ट का लेखक है या नहीं, Firestore में पोस्ट ढूंढने के लिए get का इस्तेमाल करें:

request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID

इनमें से कोई भी शर्त पूरी होने पर, लॉजिकल OR ऑपरेटर का इस्तेमाल करें:

allow delete: if
  // is comment author or moderator
  isAuthorOrModerator(resource.data, request.auth) ||
  // is blog post author
  request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;

टेस्ट फिर से चलाएं और पक्का करें कि एक और टेस्ट पास हो गया हो.

नियमों की पूरी फ़ाइल यह है:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // Returns true if user is post author or a moderator
    function isAuthorOrModerator(post, auth) {
      let isAuthor = auth.uid == post.authorUID;
      let isModerator = auth.token.isModerator == true;
      return isAuthor || isModerator;
    }

    function titleIsUnder50Chars(post) {
      return post.title.size() < 50;
    }

    // Draft blog posts
    match /drafts/{draftID} {
      // `authorUID`: string, required
      // `content`: string, optional
      // `createdAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, optional

      allow create: if
        // User is author
        request.auth.uid == request.resource.data.authorUID &&
        // Must include title, author, and createdAt fields
        request.resource.data.keys().hasAll([
          "authorUID",
          "createdAt",
          "title"
        ]) &&
        titleIsUnder50Chars(request.resource.data);

      allow update: if
        // User is author
        resource.data.authorUID == request.auth.uid &&
        // `authorUID` and `createdAt` are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "createdAt"
          ]) &&
        titleIsUnder50Chars(request.resource.data);

      // Can be read or deleted by author or moderator
      allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
    }

    // Published blog posts are denormalized from drafts
    match /published/{postID} {
      // `authorUID`: string, required
      // `content`: string, required
      // `publishedAt`: timestamp, required
      // `title`: string, < 50 characters, required
      // `url`: string, required
      // `visible`: boolean, required

      // Can be read by everyone
      allow read: if true;

      // Published posts are created only via functions, never by users
      // No hard deletes; soft deletes update `visible` field.
      allow create, delete: if false;

      allow update: if
        isAuthorOrModerator(resource.data, request.auth) &&
        // Immutable fields are unchanged
        request.resource.data.diff(resource.data).unchangedKeys().hasAll([
          "authorUID",
          "publishedAt",
          "url"
        ]) &&
        // Required fields are present
        request.resource.data.keys().hasAll([
          "content",
          "title",
          "visible"
        ]) &&
        titleIsUnder50Chars(request.resource.data);
    }

    match /published/{postID}/comments/{commentID} {
      // `authorUID`: string, required
      // `createdAt`: timestamp, required
      // `editedAt`: timestamp, optional
      // `comment`: string, < 500 characters, required

      // Must have permanent account to read comments
      allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");

      allow create: if
        // User has verified email
        request.auth.token.email_verified == true &&
        // Comment is under 500 characters
        request.resource.data.comment.size() < 500 &&
        // UID is not on the block list
        !exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

      allow update: if
        // is author
        request.auth.uid == resource.data.authorUID &&
        // within an hour of comment creation
        (request.time - resource.data.createdAt) < duration.value(1, 'h');

      allow delete: if
        // is comment author or moderator
        isAuthorOrModerator(resource.data, request.auth) ||
        // is blog post author
        request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
    }
  }
}

12. अगले चरण

बधाई हो! आपने सुरक्षा से जुड़े ऐसे नियम लिखे हैं जिनकी वजह से सभी टेस्ट पास हो गए हैं और ऐप्लिकेशन सुरक्षित हो गया है!

यहां कुछ मिलते-जुलते विषय दिए गए हैं जिनके बारे में ज़्यादा जानकारी पाई जा सकती है:

  • ब्लॉग पोस्ट: सुरक्षा नियमों की कोड समीक्षा कैसे करें
  • कोडलैब: एम्युलेटर की मदद से, लोकल फ़र्स्ट डेवलपमेंट के बारे में जानकारी
  • वीडियो: GitHub Actions का इस्तेमाल करके, एम्युलेटर पर आधारित टेस्ट के लिए सीआई को कैसे सेट अप करें