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ật và Viế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
, location
và city
. 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 hasAll
và hasOnly
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
, location
và city
, đồng thời cho phép các trường address
, hours
và cuisine
(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_score
và rating_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
, hours
và cuisine
.
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
, content
và author_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ử is
là bool
, bytes
, float
, int
, list
, latlng
, number
, path
, map
, string
và timestamp
. Toán tử is
cũng hỗ trợ các loại dữ liệu constraint
, duration
, set
và map_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 list
và map
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.