控管特定欄位的存取權

本頁內容以「建立安全性規則」和「為安全性規則編寫條件」的概念為基礎,說明如何使用 Firestore 安全性規則建立規則,允許用戶端對文件中的某些欄位執行作業,但不能對其他欄位執行作業。

有時您可能想在欄位層級控管文件變更,而非文件層級。

舉例來說,您可能想允許客戶建立或變更文件,但不允許他們編輯文件中的特定欄位。或者,您可能希望強制規定客戶建立的任何文件都必須包含特定欄位組合。本指南說明如何使用 Firestore 安全性規則完成部分工作。

僅允許特定欄位的讀取權限

Firestore 的讀取作業是在文件層級執行。系統會擷取整份文件,或完全不擷取。無法擷取部分文件。單獨使用安全性規則無法防止使用者讀取文件中的特定欄位。

如要對部分使用者隱藏文件中的特定欄位,最好的方法是將這些欄位放在另一份文件中。舉例來說,您可能會考慮在 private 子集合中建立文件,如下所示:

/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

然後,您可以為這兩個集合新增不同存取層級的安全規則。在本例中,我們使用自訂授權聲明,表示只有自訂授權聲明 role 等於 Finance 的使用者,才能查看員工的財務資訊。

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

限制文件建立作業的欄位

Firestore 沒有結構定義,也就是說,文件包含的欄位不受資料庫層級限制。雖然這種彈性可簡化開發作業,但有時您會希望確保用戶端只能建立包含特定欄位的文件,或不包含其他欄位。

您可以檢查 request.resource.data 物件的 keys 方法,建立這些規則。這是用戶端嘗試在這個新文件中寫入的所有欄位清單。將這組欄位與 hasOnly()hasAny() 等函式結合,即可加入相關邏輯,限制使用者可新增至 Firestore 的文件類型。

在新文件中要求特定欄位

假設您要確保在 restaurant 集合中建立的所有文件都包含至少一個 namelocationcity 欄位。您可以對新文件中的鍵清單呼叫 hasAll() 來完成這項操作。

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

這樣一來,餐廳就能使用其他欄位建立,但可確保用戶端建立的所有文件至少包含這三個欄位。

禁止在新文件中使用特定欄位

同樣地,您也可以使用 hasAny() 針對禁止使用的欄位清單,防止用戶端建立含有特定欄位的文件。如果文件包含任何這類欄位,這個方法就會評估為 true,因此您可能需要否定結果,禁止使用特定欄位。

舉例來說,在下列範例中,用戶端不得建立包含 average_scorerating_count 欄位的文件,因為這些欄位稍後會透過伺服器呼叫新增。

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

為新文件建立欄位許可清單

您可能不想禁止新文件中的特定欄位,而是想建立一份清單,只列出新文件中明確允許的欄位。接著,您可以使用 hasOnly() 函式,確保建立的新文件只包含這些欄位 (或這些欄位的子集),不含其他欄位。

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

合併必填和選填欄位

您可以在安全防護規則中一併使用 hasAllhasOnly 作業,要求部分欄位,並允許其他欄位。舉例來說,這個範例要求所有新文件都包含 namelocationcity 欄位,並允許使用 addresshourscuisine 欄位。

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

在實際情況中,您可能會希望將這項邏輯移至輔助函式,避免重複程式碼,並更輕鬆地將選填和必填欄位合併為單一清單,如下所示:

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

限制更新的欄位

常見的安全做法是只允許用戶端編輯部分欄位,您無法單純查看上一節所述的 request.resource.data.keys() 清單來達成此目的,因為這個清單代表更新後的文件完整樣貌,因此會包含用戶端未變更的欄位。

不過,如果您使用 diff() 函式,就可以比較 request.resource.dataresource.data 物件,後者代表更新前資料庫中的文件。這會建立 mapDiff 物件,其中包含兩個不同對應之間的變更。

您可以在這個 mapDiff 上呼叫 affectedKeys() 方法,找出編輯作業中變更的一組欄位。然後,您可以使用 hasOnly()hasAny() 等函式,確保這個集合包含 (或不包含) 特定項目。

防止變更部分欄位

affectedKeys() 產生的集合上使用 hasAny() 方法,然後否定結果,即可拒絕任何嘗試變更您不想變更的欄位的用戶端要求。

舉例來說,您可能想允許客戶更新餐廳資訊,但不能變更平均分數或評論數。

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

只允許變更特定欄位

除了指定不想變更的欄位,您也可以使用 hasOnly() 函式指定要變更的欄位清單。一般而言,這項做法更安全,因為系統預設會禁止寫入任何新的文件欄位,直到您在安全規則中明確允許為止。

舉例來說,您可以建立安全性規則,只允許用戶端變更 namelocationcityaddresshourscuisine 欄位,而不是禁止使用 average_scorerating_count 欄位。

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

也就是說,如果應用程式在日後的某個疊代版本中,餐廳文件包含 telephone 欄位,您就無法編輯該欄位,直到返回並在安全規則的 hasOnly() 清單中加入該欄位為止。

強制執行欄位類型

Firestore 沒有結構定義的另一個影響是,資料庫層級不會強制規定特定欄位可儲存的資料類型。不過,您可以使用 is 運算子,在安全性規則中強制執行這項操作。

舉例來說,下列安全性規則會強制規定評論的 score 欄位必須是整數,headlinecontentauthor_name 欄位是字串,而 review_date 則是時間戳記。

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

is 運算子的有效資料類型為 boolbytesfloatintlistlatlngnumberpathmapstringtimestampis 運算子也支援 constraintdurationsetmap_diff 資料型別,但由於這些型別是由安全性規則語言本身產生,而非由用戶端產生,因此在大多數實際應用程式中很少使用。

listmap 資料類型不支援泛型或型別引數。 換句話說,您可以使用安全規則,強制規定特定欄位包含清單或對應,但無法強制規定欄位包含所有整數或所有字串的清單。

同樣地,您可以使用安全規則,針對清單或對應中的特定項目強制執行類型值 (分別使用括號標記或鍵名),但無法一次強制執行對應或清單中所有成員的資料類型。

舉例來說,下列規則可確保文件中的 tags 欄位包含清單,且第一個項目是字串。此外,這項功能也會確保 product 欄位包含對應,而該對應又包含產品名稱 (字串) 和數量 (整數)。

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

建立及更新文件時,都必須強制執行欄位類型。 因此,建議您建立輔助函式,以便在安全性規則的建立和更新部分呼叫。

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

強制執行選填欄位的類型

請務必記得,在沒有 foo 的文件中呼叫 request.resource.data.foo 會導致錯誤,因此任何進行該呼叫的安全規則都會拒絕要求。您可以在 request.resource.data 中使用 get 方法處理這種情況。如果從地圖擷取的欄位不存在,您可以使用 get 方法為該欄位提供預設引數。

舉例來說,如果檢閱文件也包含選填的 photo_url 欄位和選填的 tags 欄位,而您想確認這些欄位分別是字串和清單,可以將 reviewFieldsAreValidTypes 函式重新編寫成類似下列內容:

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

這會拒絕含有 tags 但不是清單的文件,同時允許不含 tags (或 photo_url) 欄位的文件。

一律不允許部分寫入

最後,請注意 Firestore 安全性規則只會允許用戶端變更文件,或拒絕整項編輯作業。您無法建立安全性規則,在同一項作業中接受寫入文件中的某些欄位,同時拒絕寫入其他欄位。