Kiểm soát quyền truy cập vào các trường cụ thể

Trang này dựa trên các khái niệm trong phần Cấu trúc quy tắc bảo mậtViết điều kiện cho quy tắc bảo mật để giải thích cách bạn có thể sử dụng Cloud Firestore Security Rules để tạo các quy tắc cho phép ứng dụng thực hiện các thao tác trên một số trường trong tài liệu nhưng không thực hiện trên các trường khác.

Đôi khi, bạn muốn kiểm soát các thay đổi đối với một tài liệu không ở cấp tài liệu mà ở cấp trường.

Ví dụ: bạn có thể muốn cho phép một ứng dụng tạo hoặc thay đổi tài liệu, nhưng không cho phép ứng dụng đó chỉnh sửa một số trường trong tài liệu đó. Hoặc bạn có thể muốn thực thi rằng mọi tài liệu mà một ứng dụng luôn tạo đều chứa một bộ trường nhất định. Hướng dẫn này trình bày cách bạn có thể hoàn thành một số việc trong số này bằng cách sử dụng Cloud Firestore Security Rules.

Chỉ cho phép quyền truy cập đọc đối với các trường cụ thể

Các thao tác đọc trong Cloud Firestore được thực hiện ở cấp tài liệu. Bạn có thể truy xuất toàn bộ tài liệu hoặc không truy xuất được gì. Không có cách nào để truy xuất một phần tài liệu. Không thể chỉ dùng các quy tắc bảo mật để ngăn người dùng đọc các trường cụ thể trong một tài liệu.

Nếu có một số trường trong tài liệu mà bạn muốn ẩn khỏi một số người dùng, thì cách tốt nhất là đặt các trường đó vào một tài liệu riêng. Ví dụ: bạn có thể cân nhắc việc tạo một tài liệu trong một bộ sưu tập con private như sau:

/employees/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/employees/{emp_id}/private/finances

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

Sau đó, bạn có thể thêm các quy tắc bảo mật có nhiều cấp độ truy cập cho hai tập hợp này. Trong ví dụ này, chúng ta đang sử dụng các xác nhận quyền sở hữu uỷ quyền tuỳ chỉnh để cho biết rằng chỉ những người dùng có xác nhận quyền sở hữu uỷ quyền tuỳ chỉnh role bằng Finance mới có thể xem thông tin tài chính của nhân viên.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

Hạn chế các trường khi tạo tài liệu

Cloud Firestore không có lược đồ, tức là không có hạn chế ở cấp cơ sở dữ liệu đối với những trường mà một tài liệu chứa. Mặc dù tính linh hoạt này có thể giúp quá trình phát triển trở nên dễ dàng hơn, nhưng sẽ có những lúc bạn muốn đảm bảo rằng các ứng dụng chỉ có thể tạo tài liệu chứa các trường cụ thể hoặc không chứa các trường khác.

Bạn có thể tạo các quy tắc này bằng cách kiểm tra phương thức keys của đối tượng request.resource.data. Đây là danh sách tất cả các trường mà ứng dụng đang cố gắng ghi vào tài liệu mới này. Bằng cách kết hợp nhóm trường này với các hàm như hasOnly() hoặc hasAny(), bạn có thể thêm logic hạn chế các loại tài liệu mà người dùng có thể thêm vào Cloud Firestore.

Yêu cầu các trường cụ thể trong tài liệu mới

Giả sử bạn muốn đảm bảo rằng tất cả các tài liệu được tạo trong một bộ sưu tập restaurant đều chứa ít nhất một trường name, locationcity. Bạn có thể thực hiện việc đó bằng cách gọi hasAll() trên danh sách khoá trong tài liệu mới.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

Điều này cho phép tạo nhà hàng bằng các trường khác, nhưng đảm bảo rằng tất cả các tài liệu do một ứng dụng tạo đều chứa ít nhất 3 trường này.

Cấm các trường cụ thể trong tài liệu mới

Tương tự, bạn có thể ngăn các ứng dụng tạo tài liệu chứa các trường cụ thể bằng cách sử dụng hasAny() đối với danh sách các trường bị cấm. Phương thức này đánh giá là true nếu một tài liệu chứa bất kỳ trường nào trong số này, vì vậy, có thể bạn muốn phủ định kết quả để cấm một số trường nhất định.

Ví dụ: trong ví dụ sau, các ứng dụng không được phép tạo một tài liệu có chứa trường average_score hoặc rating_count vì các trường này sẽ được thêm bằng một lệnh gọi máy chủ vào thời điểm sau.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

Tạo danh sách cho phép các trường cho tài liệu mới

Thay vì cấm một số trường trong tài liệu mới, bạn có thể muốn tạo một danh sách chỉ gồm những trường được cho phép rõ ràng trong tài liệu mới. Sau đó, bạn có thể sử dụng hàm hasOnly() để đảm bảo rằng mọi tài liệu mới được tạo chỉ chứa các trường này (hoặc một nhóm nhỏ các trường này) và không có trường nào khác.

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Kết hợp các trường bắt buộc và không bắt buộc

Bạn có thể kết hợp các thao tác hasAllhasOnly với nhau trong quy tắc bảo mật để yêu cầu một số trường và cho phép các trường khác. Ví dụ: ví dụ này yêu cầu tất cả tài liệu mới phải chứa các trường name, locationcity, đồng thời cho phép các trường address, hourscuisine (không bắt buộc).

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Trong trường hợp thực tế, bạn có thể muốn chuyển logic này vào một hàm trợ giúp để tránh trùng lặp mã và dễ dàng kết hợp các trường không bắt buộc và bắt buộc vào một danh sách duy nhất, như sau:

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

Hạn chế các trường khi cập nhật

Một biện pháp bảo mật thường thấy là chỉ cho phép các ứng dụng chỉnh sửa một số trường chứ không phải các trường khác. Bạn không thể chỉ xem danh sách request.resource.data.keys() được mô tả trong phần trước, vì danh sách này đại diện cho toàn bộ tài liệu như tài liệu sẽ xuất hiện sau khi cập nhật và do đó sẽ bao gồm các trường mà máy khách không thay đổi.

Tuy nhiên, nếu sử dụng hàm diff(), bạn có thể so sánh request.resource.data với đối tượng resource.data. Đối tượng này đại diện cho tài liệu trong cơ sở dữ liệu trước khi cập nhật. Thao tác này sẽ tạo một đối tượng mapDiff. Đây là đối tượng chứa tất cả các thay đổi giữa 2 tập hợp map khác nhau.

Bằng cách gọi phương thức affectedKeys() trên mapDiff này, bạn có thể đưa ra một tập hợp các trường đã được thay đổi trong một thao tác chỉnh sửa. Sau đó, bạn có thể sử dụng các hàm như hasOnly() hoặc hasAny() để đảm bảo rằng tập hợp này có (hoặc không) chứa một số mục nhất định.

Ngăn chặn việc thay đổi một số trường

Bằng cách sử dụng phương thức hasAny() trên tập hợp do affectedKeys() tạo ra, rồi phủ định kết quả, bạn có thể từ chối mọi yêu cầu của máy khách cố gắng thay đổi các trường mà bạn không muốn thay đổi.

Ví dụ: bạn có thể muốn cho phép khách hàng cập nhật thông tin về một nhà hàng nhưng không được thay đổi điểm trung bình hoặc số lượng bài đánh giá.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

Chỉ cho phép thay đổi một số trường nhất định

Thay vì chỉ định các trường mà bạn không muốn thay đổi, bạn cũng có thể sử dụng hàm hasOnly() để chỉ định danh sách các trường mà bạn muốn thay đổi. Điều này thường được coi là an toàn hơn vì theo mặc định, các thao tác ghi vào bất kỳ trường tài liệu mới nào đều bị cấm cho đến khi bạn cho phép rõ ràng trong các quy tắc bảo mật.

Ví dụ: thay vì không cho phép trường average_scorerating_count, bạn có thể tạo các quy tắc bảo mật chỉ cho phép máy khách thay đổi các trường name, location, city, address, hourscuisine.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

Điều này có nghĩa là nếu trong một số lần lặp lại ứng dụng trong tương lai, tài liệu về nhà hàng có trường telephone, thì các nỗ lực chỉnh sửa trường đó sẽ không thành công cho đến khi bạn quay lại và thêm trường đó vào danh sách hasOnly() trong quy tắc bảo mật.

Thực thi các loại trường

Một hiệu ứng khác của Cloud Firestore là không có lược đồ, tức là không có hoạt động thực thi ở cấp cơ sở dữ liệu đối với những loại dữ liệu có thể được lưu trữ trong các trường cụ thể. Đây là điều bạn có thể thực thi trong các quy tắc bảo mật, tuy nhiên, với toán tử is.

Ví dụ: quy tắc bảo mật sau đây thực thi rằng trường score của bài đánh giá phải là một số nguyên, các trường headline, contentauthor_name là chuỗi và review_date là dấu thời gian.

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

Các kiểu dữ liệu hợp lệ cho toán tử isbool, bytes, float, int, list, latlng, number, path, map, stringtimestamp. Toán tử is cũng hỗ trợ các loại dữ liệu constraint, duration, setmap_diff, nhưng vì các loại dữ liệu này do chính ngôn ngữ quy tắc bảo mật tạo ra chứ không phải do các ứng dụng tạo ra, nên bạn hiếm khi sử dụng chúng trong hầu hết các ứng dụng thực tế.

Loại dữ liệu listmap không hỗ trợ kiểu chung hoặc đối số kiểu. Nói cách khác, bạn có thể sử dụng các quy tắc bảo mật để thực thi rằng một trường nhất định chứa một danh sách hoặc một bản đồ, nhưng bạn không thể thực thi rằng một trường chứa một danh sách gồm tất cả các số nguyên hoặc tất cả các chuỗi.

Tương tự, bạn có thể sử dụng các quy tắc bảo mật để thực thi các giá trị kiểu cho các mục cụ thể trong danh sách hoặc bản đồ (tương ứng bằng cách sử dụng ký hiệu dấu ngoặc hoặc tên khoá), nhưng không có lối tắt để thực thi các kiểu dữ liệu của tất cả các thành viên trong bản đồ hoặc danh sách cùng một lúc.

Ví dụ: các quy tắc sau đây đảm bảo rằng trường tags trong một tài liệu chứa một danh sách và mục đầu tiên là một chuỗi. Thao tác này cũng đảm bảo rằng trường product chứa một bản đồ, bản đồ này lại chứa tên sản phẩm là một chuỗi và số lượng là một số nguyên.

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

Bạn cần thực thi các loại trường khi tạo và cập nhật tài liệu. Do đó, bạn nên cân nhắc việc tạo một hàm trợ giúp mà bạn có thể gọi trong cả phần tạo và cập nhật của quy tắc bảo mật.

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

Thực thi các loại cho trường không bắt buộc

Bạn cần lưu ý rằng việc gọi request.resource.data.foo trên một tài liệu mà foo không tồn tại sẽ dẫn đến lỗi. Do đó, mọi quy tắc bảo mật thực hiện lệnh gọi đó sẽ từ chối yêu cầu. Bạn có thể xử lý tình huống này bằng cách sử dụng phương thức get trên request.resource.data. Phương thức get cho phép bạn cung cấp một đối số mặc định cho trường mà bạn đang truy xuất từ bản đồ nếu trường đó không tồn tại.

Ví dụ: nếu tài liệu đánh giá cũng chứa một trường photo_url không bắt buộc và một trường tags không bắt buộc mà bạn muốn xác minh là chuỗi và danh sách tương ứng, bạn có thể thực hiện việc này bằng cách viết lại hàm reviewFieldsAreValidTypes thành một hàm như sau:

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

Điều này sẽ từ chối những tài liệu có tags nhưng không phải là danh sách, đồng thời vẫn cho phép những tài liệu không chứa trường tags (hoặc photo_url).

Không bao giờ được phép ghi một phần

Một lưu ý cuối cùng về Cloud Firestore Security Rules là chúng cho phép ứng dụng thực hiện thay đổi đối với tài liệu hoặc từ chối toàn bộ nội dung chỉnh sửa. Bạn không thể tạo các quy tắc bảo mật chấp nhận thao tác ghi vào một số trường trong tài liệu của mình trong khi từ chối các trường khác trong cùng một thao tác.