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 bài viết Xây dựng 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 nhằm 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 được 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 tài liệu không phải ở cấp tài liệu mà ở cấp trường.

Ví dụ: bạn có thể muốn cho phép ứng dụng khách 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 nhất định trong tài liệu đó. Hoặc bạn nên thực thi mọi tài liệu mà ứng dụng luôn tạo đều chứa một tập hợp 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ố nhiệm vụ 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 một số trường

Các hoạt động đọc trong Cloud Firestore được thực hiện ở cấp tài liệu. Bạn truy xuất toàn bộ tài liệu hoặc không truy xuất gì. Không có cách nào để truy xuất một phần tài liệu. Không thể chỉ sử 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 tài liệu.

Nếu có một số trường nhất định 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 tập hợ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ó các cấp truy cập khác nhau cho hai bộ sưu tập. Trong ví dụ này, chúng tôi sử dụng thông báo xác nhận quyền sở hữu tuỳ chỉnh để cho biết rằng chỉ những người dùng có thông báo xác nhận quyền sở hữu 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 trong quá trình tạo tài liệu

Cloud Firestore không có giản đồ, nghĩa là không có quy định hạn chế nào ở cấp cơ sở dữ liệu đối với các 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 dễ dàng hơn, nhưng đôi khi bạn muốn đảm bảo rằng ứ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 tập hợp các 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 bộ sưu tập restaurant chứa ít nhất một trường name, locationcity. Bạn có thể thực hiện việc này bằng cách gọi hasAll() trong danh sách các 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 cũng 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ả tài liệu do ứ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 ứ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() so với danh sách các trường bị cấm. Phương thức này sẽ đánh giá là true nếu tài liệu chứa bất kỳ trường nào trong số này. Vì vậy, bạn có thể muốn phủ định kết quả để cấm một số trường nhất định.

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

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 chứa các trường đối với tài liệu mới

Thay vì cấm một số trường nhất định trong tài liệu mới, bạn nên tạo một danh sách chỉ bao gồm những trường được 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 nê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 phương pháp bảo mật phổ biến là chỉ cho phép ứng dụng chỉnh sửa một số trường chứ không chỉnh sửa các trường khác. Bạn không thể thực hiện việc này chỉ bằng cá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 tài liệu hoàn chỉnh sau khi cập nhật và do đó sẽ bao gồm các trường mà ứng dụng 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 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à một đối tượng chứa tất cả các thay đổi giữa hai bản đồ khác nhau.

Bằng cách gọi phương thức affectedKeys() trên mapDiff này, bạn có thể tạo một tập hợp các trường đã thay đổi trong một lần 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 có) một số mục nhất định.

Không cho phép 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, sau đó phủ định kết quả, bạn có thể từ chối mọi yêu cầu của ứng dụng cố gắng thay đổi các trường mà bạn không muốn thay đổi.

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

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. Phương thức này thường được coi là bảo mật 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 không được cho phép cho đến khi bạn cho phép rõ ràng các thao tác đó trong 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 cho phép ứng dụng 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 tương lai, tài liệu về nhà hàng có trường telephone cố gắng 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 loại trường

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

Ví dụ: quy tắc bảo mật sau đây thực thi việc trường score của bài đánh giá phải là số nguyên, 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 loại 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 và không phải do ứng dụng tạo, nên bạn hiếm khi sử dụng các loại dữ liệu này trong hầu hết các ứng dụng thực tế.

Các loại dữ liệu listmap không hỗ trợ các loại chung hoặc đối số loại. 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 việc một trường nhất định chứa danh sách hoặc bản đồ, nhưng bạn không thể thực thi việc một trường chứa danh sách tất cả số nguyên hoặc tất 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 giá trị loại 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 nào để thực thi các loại dữ liệu của tất 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. Hàm này cũng đảm bảo rằng trường product chứa một bản đồ, bản đồ này 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 phải 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 tạo một hàm trợ giúp mà bạn có thể gọi trong cả phần tạo và phần cập nhật của các 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 các trường không bắt buộc

Điều quan trọng cần nhớ là việc gọi request.resource.data.foo trên một tài liệu không có foo sẽ dẫn đến lỗi. Do đó, mọi quy tắc bảo mật tạo ra 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 đối số mặc định cho trường mà bạn đang truy xuất từ mộ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à các chuỗi và danh sách tương ứng, thì 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 nội dung 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 các tài liệu có tags nhưng không phải là danh sách, đồng thời vẫn cho phép các tài liệu không chứa trường tags (hoặc photo_url).

Không bao giờ cho phép ghi một phần

Một lưu ý cuối cùng về Cloud Firestore Security Rules là các phương thức này cho phép ứng dụng thực hiện thay đổi đối với một 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 quy tắc bảo mật chấp nhận ghi vào một số trường trong tài liệu trong khi từ chối các trường khác trong cùng một thao tác.