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

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

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 trên 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 giản đồ cho ứng dụng của bạn. Đây là một trong những phần quan trọng nhất của việc phát triển ứng dụng của bạn. Và bảng mã này sẽ hướng dẫn bạn qua nó.

Đ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 này ; để kiểm tra phiên bản của bạn, hãy chạy java -version )

Bạn sẽ làm gì

Trong bảng mã này, bạn sẽ đảm bảo 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 giả lập Firestore để chạy các bài kiểm tra đơ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 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à nhập
  • 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
  • Triển khai danh sách từ chối và xóa mềm
  • Hiểu khi nào cần 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 của ứng dụng:

Các bài đăng trên blog nháp:

  • Người dùng có thể tạo các bài đăng trên blog nháp, nằm trong bộ sưu tập các 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, một Chức năng Firebase được kích hoạt để tạo một 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, chỉ thông qua một chức năng.
  • Chúng chỉ có thể được xóa mềm để cập nhật một thuộc tính visible thành false.

Bình luận

  • Các bài đăng đã xuất bản cho phép nhận xét, là một bộ sưu tập phụ trên mỗi bài đăng đã xuất bản.
  • Để giảm lạm dụng, người dùng phải có địa chỉ email đã được xác minh và không thuộc danh sách từ chối để có thể để lại nhận xét.
  • Nhận xét chỉ có thể được cập nhật trong vòng một giờ sau khi nó đượ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 Firebase Emulator Suite.

Lấy mã nguồn

Trong codelab này, bạn sẽ bắt đầu với các thử nghiệm cho Quy tắc bảo mật, nhưng chính Quy tắc bảo mật mimimal, 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 vào thư mục trạng thái ban đầu, nơi bạn sẽ làm việc cho phần còn lại của bảng mã này:

$ cd codelab-rules/initial-state

Bây giờ, hãy cài đặt các phụ thuộc để bạn có thể chạy các bài kiểm tra. Nếu bạn đang 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 của Firebase

Bộ giả lập bạn sẽ sử dụng để chạy các bài kiểm tra 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. Bộ mã 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 bao gồm nhiều bản sửa lỗi hơn.

$ firebase --version
9.10.2

3. Chạy các bài kiểm tra

Trong phần này, bạn sẽ chạy các bài kiểm tra cục bộ. Điều này có nghĩa là đã đến lúc khởi động Bộ giả lập.

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 chính của Firestore: drafts chứa các bài đăng trên 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 bộ sưu tập con về các bài đăng đã xuất bản. Repo đ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 trong bộ sưu tập drafts , 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 bài kiểm tra đề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 soạn thảo của bạn.

Trên dòng lệnh, khởi động trình giả lập bằng emulators:exec thi hành và chạy các bài kiểm tra:

$ 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 thất bại. Khi tạo tệp quy tắc, bạn có thể đo lường tiến trình bằng cách xem nhiều bài kiểm tra vượt qua.

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

Bởi vì quyền truy cập cho các bài đăng trên blog nháp rất khác với quyền truy cập cho các bài đăng trên 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 nhận cho các trường bắt buộc và bất biến.

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ả các tài liệu trong tập hợp con. Và bởi vì nó ở cấp cao nhất, hiện tại, cùng một quy tắc chung áp dụng cho tất cả các yêu cầu, bất kể ai đang đưa ra yêu cầu hoặc dữ liệu họ đang cố gắng đọc hoặc ghi.

Bắt đầu bằng cách xóa câu lệnh khớp nhất bên trong 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 bảng mã 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 sẽ 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, 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 trường đó trước khi cố gắng 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, bạn có thể kiểm tra yêu cầu đó. request.resource có tất cả những chìa khóa:

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 hơn 50 ký tự:

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

Vì tất cả các điều kiện này phải đúng, hãy nối các điều kiện này 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, 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 trên blog nháp của họ, 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 đăng có thể được cập nhật. Đầu tiên, chỉ tác giả mới có thể cập nhật bản nháp của họ. 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ó 50 ký tự trở xuống:

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

Vì tất cả các điều kiện này cần được đáp ứng, hãy nối chúng 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

Cũng 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 các bản nháp:

request.auth.token.isModerator == true

Vì một trong hai điều kiện này là đủ để xóa, hãy nối chúng với một 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, do đó, quyền có thể được thêm 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 đủ hiện 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 mẫu truy cập khác nhau

Bởi vì các mẫu truy cập cho 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 không 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 các 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 đã xuất bản.

Tiếp theo, bạn sẽ viết các quy tắc cho các bài đăng đã xuất bản. Các quy tắc đơn giản nhất để viết là các bài đăng đã xuất bản có thể được đọc bởi bất kỳ ai và không thể được tạo hoặc xóa bởi bất kỳ ai. 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 các quy tắc 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 đã vượt qua.

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

Các điều kiện để cập nhật mộ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 kiểm duyệt 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, 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à khó duy trì. Thay vào đó, bạn sẽ tạo một hàm tùy chỉnh đóng gói 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 gọi là isAuthorOrModerator lấy làm đối số cho tài liệu bài đăng (điều này sẽ hoạt động đối với bản nháp hoặc bài đăng đã xuất bản) và đối tượng auth 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 một câu lệnh trả về và của chúng ta sẽ trả về một boolean cho biết nếu một trong hai biến là true:

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 mộ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 thực

Không nên thay đổi một số trường của một bài đăng đã xuất bản, cụ thể là các trường url , authorUIDpublishedAt là bất biến. Hai trường còn lại, titlecontent , và visible vẫn phải xuất hiện 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 các bản cập nhật cho 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ạo một chức năng tùy chỉnh của riêng bạn

Và cuối cùng, hãy thêm một điều kiện là tiêu đề phải dưới 50 ký tự. Bởi vì đây là logic được sử dụng lại, 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 một bài đăng đã xuất bản 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 thi đạt và 4 bài thi không đạ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 bộ sưu tậ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 một 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 áp dụng cho các 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 các nhận xét, hãy bắt đầu với câu lệnh 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 vĩnh viễn, 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 sẽ vượt qua.

Tạo nhận xét: Kiểm tra danh sách từ chối

Có ba điều kiện để tạo nhận xét:

  • một người dùng phải có một 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 cửa hàng lửa trong bộ bannedUsers tập Người dùng bị cấm. Thực hiện các điều kiện này tại một thời điểm:
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 nhận xét 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 charachters
        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 rằng một bài kiểm tra nữa sẽ vượt qua.

10. Cập nhật nhận xét: Quy tắc dựa trên thời gian

Logic kinh doanh đối với nhận xét là chúng có thể được tác giả nhận xét chỉnh sửa 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 .

Trước 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 chúng 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 rằng một bài kiểm tra nữa sẽ vượt qua.

11. Xóa nhận xét: 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ì chức năng trợ giúp mà bạn đã thêm trước đó kiểm tra trường authorUID có thể tồn tại trên bài đăng hoặc nhận xét, bạn có thể sử dụng lại chức năng trợ giúp để kiểm tra xem người dùng có phải là tác giả hay người kiểm duyệt hay không:

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, get sử dụng chức năng tìm kiếm bài đăng trong Firestore:

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

Bởi vì bất kỳ điều kiện nào trong số này là đủ, hãy sử dụng toán tử OR logic 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 rằng một bài kiểm tra nữa sẽ vượt qua.

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

Xin chúc mừng! Bạn đã viết Quy tắc bảo mật giúp vượt qua tất cả các bài kiểm tra 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 : Làm thế nào để đánh giá mã Quy tắc bảo mật
  • Codelab : xem xét quá trình phát triển đầu tiên tại địa phương với Trình giả lập
  • Video : Cách sử dụng thiết lập CI cho các bài kiểm tra dựa trên trình mô phỏng bằng GitHub Actions