Bảo vệ dữ liệu Firestore của bạn bằng Quy tắc bảo mật Firebase

1. Trước khi bạn bắt đầu

Cloud Firestore, Cloud Storage cho Firebase và Cơ sở dữ liệu thời gian thực dựa vào các tệp cấu hình bạn viết để cấp quyền truy cập đọc và ghi. Cấu hình đó, được gọi là Quy tắc bảo mật, cũng có thể hoạt động như một loại lược đồ cho ứng dụng của bạn. Đây là một trong những phần quan trọng nhất trong việc phát triển ứng dụng của bạn. Và lớp học lập trình này sẽ hướng dẫn bạn thực hiện điều đó.

Điều kiện tiên quyết

  • Một trình soạn thảo đơn giản như Visual Studio Code, Atom hoặc Sublime Text
  • Node.js 8.6.0 trở lên (để cài đặt Node.js, hãy sử dụng nvm ; để kiểm tra phiên bản của bạn, hãy chạy node --version )
  • Java 7 trở lên (để cài đặt Java, hãy sử dụng các hướng dẫn sau ; để kiểm tra phiên bản của bạn, hãy chạy java -version )

Bạn sẽ làm gì

Trong lớp học lập trình này, bạn sẽ bảo mật một nền tảng blog đơn giản được xây dựng trên Firestore. Bạn sẽ sử dụng trình mô phỏng Firestore để chạy thử nghiệm đơn vị theo Quy tắc bảo mật và đảm bảo rằng các quy tắc cho phép và không cho phép quyền truy cập mà bạn mong đợi.

Bạn sẽ học cách:

  • Cấp quyền chi tiết
  • Thực thi xác thực dữ liệu và loại
  • Triển khai kiểm soát truy cập dựa trên thuộc tính
  • Cấp quyền truy cập dựa trên phương thức xác thực
  • Tạo các chức năng tùy chỉnh
  • Tạo quy tắc bảo mật dựa trên thời gian
  • Thực hiện danh sách từ chối và xóa mềm
  • Hiểu thời điểm không chuẩn hóa dữ liệu để đáp ứng nhiều kiểu truy cập

2. Thiết lập

Đây là một ứng dụng viết blog. Đây là bản tóm tắt cấp cao về chức năng ứng dụng:

Bài đăng trên blog dự thảo:

  • Người dùng có thể tạo các bài đăng blog nháp nằm trong bộ sưu tập drafts .
  • Tác giả có thể tiếp tục cập nhật bản nháp cho đến khi nó sẵn sàng được xuất bản.
  • Khi nó sẵn sàng được xuất bản, Chức năng Firebase được kích hoạt để tạo tài liệu mới trong bộ sưu tập published .
  • Bản nháp có thể bị xóa bởi tác giả hoặc bởi người kiểm duyệt trang web

Các bài đăng trên blog đã xuất bản:

  • Người dùng không thể tạo các bài đăng đã xuất bản mà chỉ có thể tạo thông qua một chức năng.
  • Chúng chỉ có thể bị xóa mềm, cập nhật thuộc tính visible thành sai.

Bình luận

  • Các bài đăng đã xuất bản cho phép nhận xét, đây là một bộ sưu tập con trên mỗi bài đăng được xuất bản.
  • Để giảm thiểu sự lạm dụng, người dùng phải có địa chỉ email đã được xác minh và không nằm trong danh sách từ chối thì mới có thể để lại nhận xét.
  • Bình luận chỉ có thể được cập nhật trong vòng một giờ sau khi được đăng.
  • Nhận xét có thể bị xóa bởi tác giả nhận xét, tác giả của bài đăng gốc hoặc bởi người kiểm duyệt.

Ngoài các quy tắc truy cập, bạn sẽ tạo Quy tắc bảo mật để thực thi các trường bắt buộc và xác thực dữ liệu.

Mọi thứ sẽ diễn ra cục bộ bằng cách sử dụng Bộ mô phỏng Firebase.

Lấy mã nguồn

Trong lớp học lập trình này, bạn sẽ bắt đầu bằng các thử nghiệm dành cho Quy tắc bảo mật, nhưng bản thân các Quy tắc bảo mật gần giống nhau, vì vậy, điều đầu tiên bạn cần làm là sao chép nguồn để chạy thử nghiệm:

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

Sau đó, chuyển sang thư mục trạng thái ban đầu, nơi bạn sẽ làm việc trong phần còn lại của lớp học lập trình này:

$ cd codelab-rules/initial-state

Bây giờ, hãy cài đặt các phần phụ thuộc để bạn có thể chạy thử nghiệm. Nếu bạn sử dụng kết nối Internet chậm hơn, quá trình này có thể mất một hoặc hai phút:

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

Nhận CLI Firebase

Bộ mô phỏng mà bạn sẽ sử dụng để chạy thử nghiệm là một phần của Firebase CLI (giao diện dòng lệnh) có thể được cài đặt trên máy của bạn bằng lệnh sau:

$ npm install -g firebase-tools

Tiếp theo, xác nhận rằng bạn có phiên bản CLI mới nhất. Lớp học lập trình này sẽ hoạt động với phiên bản 8.4.0 trở lên nhưng các phiên bản mới hơn có nhiều bản sửa lỗi hơn.

$ firebase --version
9.10.2

3. Chạy thử nghiệm

Trong phần này, bạn sẽ chạy thử nghiệm cục bộ. Điều này có nghĩa là đã đến lúc khởi động Emulator Suite.

Khởi động trình giả lập

Ứng dụng bạn sẽ làm việc có ba bộ sưu tập Firestore chính: drafts chứa các bài đăng blog đang được xử lý, bộ sưu tập published chứa các bài đăng blog đã được xuất bản và comments là một tập hợp con trên các bài đăng đã xuất bản. Kho lưu trữ đi kèm với các bài kiểm tra đơn vị cho Quy tắc bảo mật xác định thuộc tính người dùng và các điều kiện khác cần thiết để người dùng tạo, đọc, cập nhật và xóa tài liệu ở drafts , bộ sưu tập publishedcomments . Bạn sẽ viết Quy tắc bảo mật để vượt qua các bài kiểm tra đó.

Để bắt đầu, cơ sở dữ liệu của bạn đã bị khóa: việc đọc và ghi vào cơ sở dữ liệu bị từ chối trên toàn cầu và tất cả các thử nghiệm đều thất bại. Khi bạn viết Quy tắc bảo mật, các bài kiểm tra sẽ vượt qua. Để xem các bài kiểm tra, hãy mở functions/test.js trong trình chỉnh sửa của bạn.

Trên dòng lệnh, khởi động trình mô phỏng bằng cách sử dụng emulators:exec và chạy thử nghiệm:

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

Cuộn lên đầu đầu ra:

$ 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

...

Hiện tại có 9 lỗi. Khi xây dựng tệp quy tắc, bạn có thể đo lường tiến độ bằng cách xem nhiều bài kiểm tra vượt qua hơn.

4. Tạo bản nháp bài đăng trên blog.

Bởi vì quyền truy cập vào các bài đăng blog nháp rất khác với quyền truy cập vào các bài đăng blog đã xuất bản, ứng dụng viết blog này lưu trữ các bài đăng blog nháp trong một bộ sưu tập riêng biệt, /drafts . Bản nháp chỉ có thể được truy cập bởi tác giả hoặc người kiểm duyệt và có xác thực cho các trường bắt buộc và không thể thay đổi.

Mở tệp firestore.rules , bạn sẽ tìm thấy tệp quy tắc mặc định:

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

Câu lệnh so khớp, match /{document=**} , đang sử dụng cú pháp ** để áp dụng đệ quy cho tất cả tài liệu trong các bộ sưu tập con. Và bởi vì nó ở cấp cao nhất nên hiện tại, cùng một quy tắc chung sẽ áp dụng cho tất cả các yêu cầu, bất kể ai là người đưa ra yêu cầu hay họ đang cố đọc hoặc ghi dữ liệu gì.

Bắt đầu bằng cách xóa câu lệnh so khớp trong cùng và thay thế bằng câu lệnh match /drafts/{draftID} . (Nhận xét về cấu trúc của tài liệu có thể hữu ích trong các quy tắc và sẽ được đưa vào lớp học lập trình này; chúng luôn là tùy chọn.)

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
    }
  }
}

Quy tắc đầu tiên bạn viết cho bản nháp sẽ kiểm soát ai có thể tạo tài liệu. Trong ứng dụng này, bản nháp chỉ có thể được tạo bởi người được liệt kê là tác giả. Kiểm tra xem UID của người đưa ra yêu cầu có giống với UID được liệt kê trong tài liệu hay không.

Điều kiện đầu tiên để tạo sẽ là:

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

Tiếp theo, tài liệu chỉ có thể được tạo nếu chúng bao gồm ba trường bắt buộc là authorUID , createdAttitle . (Người dùng không cung cấp trường createdAt ; điều này buộc ứng dụng phải thêm nó trước khi thử tạo tài liệu.) Vì bạn chỉ cần kiểm tra xem các thuộc tính có đang được tạo hay không, nên bạn có thể kiểm tra xem request.resource có tất cả những phím đó:

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

Yêu cầu cuối cùng để tạo một bài đăng trên blog là tiêu đề không được dài quá 50 ký tự:

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

Vì tất cả các điều kiện này phải đúng, nên hãy ghép các điều kiện này lại với nhau bằng toán tử logic AND, && . Quy tắc đầu tiên trở thành:

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;
    }
  }
}

Trong thiết bị đầu cuối, hãy chạy lại các bài kiểm tra và xác nhận rằng bài kiểm tra đầu tiên đã vượt qua.

5. Cập nhật bản nháp bài đăng trên blog.

Tiếp theo, khi các tác giả tinh chỉnh các bài đăng blog nháp của mình, họ sẽ chỉnh sửa các tài liệu nháp. Tạo quy tắc cho các điều kiện khi bài viết có thể được cập nhật. Đầu tiên, chỉ có tác giả mới có thể cập nhật bản nháp của mình. Lưu ý rằng ở đây bạn kiểm tra UID đã được viết, resource.data.authorUID :

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

Yêu cầu thứ hai đối với bản cập nhật là hai thuộc tính, authorUIDcreatedAt không được thay đổi:

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

Và cuối cùng, tiêu đề phải có từ 50 ký tự trở xuống:

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

Vì tất cả các điều kiện này đều cần được đáp ứng, hãy nối chúng lại với nhau bằng && :

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;

Các quy tắc hoàn chỉnh trở thành:

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;
    }
  }
}

Chạy lại các bài kiểm tra của bạn và xác nhận rằng một bài kiểm tra khác đã vượt qua.

6. Xóa và đọc bản nháp: Kiểm soát truy cập dựa trên thuộc tính

Giống như tác giả có thể tạo và cập nhật bản nháp, họ cũng có thể xóa bản nháp.

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

Ngoài ra, các tác giả có thuộc tính isModerator trên mã thông báo xác thực của họ được phép xóa bản nháp:

request.auth.token.isModerator == true

Vì một trong hai điều kiện này đều đủ để xóa, nên hãy nối chúng bằng toán tử OR logic, || :

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

Các điều kiện tương tự áp dụng cho các lần đọc để có thể thêm quyền vào quy tắc:

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

Các quy tắc đầy đủ bây giờ là:

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;
    }
  }
}

Chạy lại các bài kiểm tra của bạn và xác nhận rằng một bài kiểm tra khác hiện đã vượt qua.

7. Đọc, tạo và xóa các bài đăng đã xuất bản: không chuẩn hóa cho các kiểu truy cập khác nhau

Vì kiểu truy cập của các bài đăng đã xuất bản và bài đăng nháp rất khác nhau nên ứng dụng này sẽ chuẩn hóa các bài đăng thành các bộ sưu tập draftpublished riêng biệt. Ví dụ: bất kỳ ai cũng có thể đọc các bài đăng đã xuất bản nhưng không thể xóa cứng, trong khi bản nháp có thể bị xóa nhưng chỉ tác giả và người kiểm duyệt mới có thể đọc được. Trong ứng dụng này, khi người dùng muốn xuất bản một bài đăng blog nháp, một chức năng sẽ được kích hoạt để tạo bài đăng mới được xuất bản.

Tiếp theo, bạn sẽ viết các quy tắc cho các bài đăng được xuất bản. Quy tắc viết đơn giản nhất là bất kỳ ai cũng có thể đọc các bài đăng đã xuất bản và không ai có thể tạo hoặc xóa các bài đăng đó. Thêm các quy tắc sau:

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;
}

Thêm những điều này vào các quy tắc hiện có, toàn bộ tệp quy tắc sẽ trở thành:

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;
    }
  }
}

Chạy lại các bài kiểm tra và xác nhận rằng một bài kiểm tra khác đã đạt.

8. Cập nhật các bài đăng đã xuất bản: Hàm tùy chỉnh và biến cục bộ

Các điều kiện để cập nhật bài đăng đã xuất bản là:

  • nó chỉ có thể được thực hiện bởi tác giả hoặc người điều hành, và
  • nó phải chứa tất cả các trường bắt buộc.

Vì bạn đã viết các điều kiện để trở thành tác giả hoặc người kiểm duyệt nên bạn có thể sao chép và dán các điều kiện, nhưng theo thời gian, điều đó có thể trở nên khó đọc và duy trì. Thay vào đó, bạn sẽ tạo một hàm tùy chỉnh gói gọn logic để trở thành tác giả hoặc người kiểm duyệt. Sau đó, bạn sẽ gọi nó từ nhiều điều kiện.

Tạo một chức năng tùy chỉnh

Phía trên câu lệnh so khớp cho bản nháp, hãy tạo một hàm mới có tên isAuthorOrModerator lấy làm đối số là tài liệu bài đăng (điều này sẽ hoạt động cho cả bản nháp hoặc bài đăng đã xuất bản) và đối tượng xác thực của người dùng:

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: ...
    }
  }
}

Sử dụng các biến cục bộ

Bên trong hàm, sử dụng từ khóa let để đặt các biến isAuthorisModerator . Tất cả các hàm phải kết thúc bằng câu lệnh return và hàm của chúng ta sẽ trả về một boolean cho biết một trong hai biến có đúng hay không:

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

Gọi hàm

Bây giờ, bạn sẽ cập nhật quy tắc cho bản nháp để gọi hàm đó, hãy cẩn thận chuyển vào resource.data làm đối số đầu tiên:

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

Bây giờ bạn có thể viết điều kiện để cập nhật các bài đăng đã xuất bản cũng sử dụng chức năng mới:

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

Thêm xác nhận

Không nên thay đổi một số trường của bài đăng đã xuất bản, cụ thể là các trường url , authorUIDpublishedAt là không thể thay đổi. Hai trường còn lại, title , contentvisible vẫn phải có sau khi cập nhật. Thêm điều kiện để thực thi các yêu cầu này đối với việc cập nhật các bài đăng đã xuất bản:

// 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"
])

Tự tạo một chức năng tùy chỉnh

Và cuối cùng, thêm điều kiện là tiêu đề dưới 50 ký tự. Vì đây là logic được sử dụng lại nên bạn có thể thực hiện việc này bằng cách tạo một hàm mới, titleIsUnder50Chars . Với chức năng mới, điều kiện cập nhật bài viết đã đăng trở thành:

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);

Và tệp quy tắc hoàn chỉnh là:

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);
    }
  }
}

Chạy lại các bài kiểm tra. Tại thời điểm này, bạn sẽ có 5 bài kiểm tra đạt và 4 bài kiểm tra trượt.

9. Nhận xét: Bộ sưu tập con và quyền của nhà cung cấp đăng nhập

Các bài đăng đã xuất bản cho phép nhận xét và các nhận xét được lưu trữ trong một tập hợp con của bài đăng đã xuất bản ( /published/{postID}/comments/{commentID} ). Theo mặc định, các quy tắc của bộ sưu tập không áp dụng cho các bộ sưu tập con. Bạn không muốn các quy tắc tương tự áp dụng cho tài liệu gốc của bài đăng đã xuất bản cũng áp dụng cho nhận xét; bạn sẽ tạo ra những cái khác nhau.

Để viết quy tắc truy cập nhận xét, hãy bắt đầu bằng câu lệnh so khớp:

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

Đọc bình luận: Không thể ẩn danh

Đối với ứng dụng này, chỉ những người dùng đã tạo tài khoản cố định, không phải tài khoản ẩn danh mới có thể đọc bình luận. Để thực thi quy tắc đó, hãy tra cứu thuộc tính sign_in_provider trên mỗi đối tượng auth.token :

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

Chạy lại các bài kiểm tra của bạn và xác nhận rằng một bài kiểm tra nữa đã vượt qua.

Tạo bình luận: Kiểm tra danh sách từ chối

Có ba điều kiện để tạo bình luận:

  • người dùng phải có email được xác minh
  • nhận xét phải ít hơn 500 ký tự và
  • họ không thể nằm trong danh sách người dùng bị cấm, được lưu trữ trong firestore trong bộ sưu tập bannedUsers . Thực hiện lần lượt các điều kiện sau:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));

Quy tắc cuối cùng để tạo bình luận là:

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));

Toàn bộ tệp quy tắc bây giờ là:

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));
    }
  }
}

Chạy lại các bài kiểm tra và đảm bảo vượt qua một bài kiểm tra nữa.

10. Cập nhật bình luận: Quy tắc theo thời gian

Logic nghiệp vụ của nhận xét là tác giả nhận xét có thể chỉnh sửa chúng trong một giờ sau khi tạo. Để thực hiện điều này, hãy sử dụng dấu thời gian createdAt .

Đầu tiên, để xác định rằng người dùng là tác giả:

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

Tiếp theo, nhận xét đó đã được tạo trong vòng một giờ qua:

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

Kết hợp những điều này với toán tử logic AND, quy tắc cập nhật nhận xét sẽ trở thành:

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');

Chạy lại các bài kiểm tra và đảm bảo vượt qua một bài kiểm tra nữa.

11. Xóa bình luận: kiểm tra quyền sở hữu của phụ huynh

Nhận xét có thể bị xóa bởi tác giả nhận xét, người kiểm duyệt hoặc tác giả của bài đăng trên blog.

Đầu tiên, vì hàm trợ giúp mà bạn đã thêm trước đó sẽ kiểm tra trường authorUID có thể tồn tại trên bài đăng hoặc nhận xét, nên bạn có thể sử dụng lại hàm trợ giúp để kiểm tra xem người dùng là tác giả hay người kiểm duyệt:

isAuthorOrModerator(resource.data, request.auth)

Để kiểm tra xem người dùng có phải là tác giả bài đăng trên blog hay không, hãy sử dụng get để tra cứu bài đăng trong Firestore:

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

Vì bất kỳ điều kiện nào trong số này đều đủ nên hãy sử dụng toán tử logic OR giữa chúng:

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;

Chạy lại các bài kiểm tra và đảm bảo vượt qua một bài kiểm tra nữa.

Và toàn bộ tệp quy tắc là:

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. Các bước tiếp theo

Chúc mừng! Bạn đã viết Quy tắc bảo mật giúp tất cả các bài kiểm tra vượt qua và bảo mật ứng dụng!

Dưới đây là một số chủ đề liên quan để đi sâu vào tiếp theo:

  • Bài đăng trên blog : Cách đánh giá mã Quy tắc bảo mật
  • Codelab : tìm hiểu quá trình phát triển đầu tiên cục bộ với Trình mô phỏng
  • Video : Cách sử dụng CI thiết lập cho các thử nghiệm dựa trên trình mô phỏng bằng cách sử dụng Tác vụ GitHub