Контролируйте доступ к определенным полям

На этой странице, основанной на концепциях, изложенных в разделах «Структурирование правил безопасности» и «Написание условий для правил безопасности», объясняется, как можно использовать Cloud Firestore Security Rules для создания правил, которые позволяют клиентам выполнять операции с некоторыми полями в документе, но не с другими.

Могут возникнуть ситуации, когда вам потребуется контролировать изменения в документе не на уровне документа, а на уровне полей.

Например, вы можете разрешить клиенту создавать или изменять документ, но запретить ему редактировать определённые поля в этом документе. Или же вы можете потребовать, чтобы любой создаваемый клиентом документ всегда содержал определённый набор полей. В этом руководстве рассматривается, как выполнить некоторые из этих задач с помощью Cloud Firestore Security Rules .

Разрешение доступа на чтение только для определенных полей

Чтение в Cloud 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'
      }
    }
  }
}

Ограничение полей при создании документа

Cloud Firestore не имеет схем, что означает отсутствие ограничений на уровне базы данных на поля, содержащиеся в документе. Хотя такая гибкость может упростить разработку, иногда может потребоваться, чтобы клиенты могли создавать документы только с определёнными полями или без других полей.

Вы можете создать эти правила, изучив метод keys объекта request.resource.data . Это список всех полей, которые клиент пытается записать в новый документ. Объединяя этот набор полей с функциями, такими как hasOnly() или hasAny() , вы можете добавить логику, ограничивающую типы документов, которые пользователь может добавлять в Cloud Firestore .

Требование определенных полей в новых документах

Допустим, вы хотите убедиться, что все документы, созданные в коллекции restaurant содержат как минимум поля name , location » и city . Это можно сделать, вызвав 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_score или rating_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']));
    }
  }
}

Объединение обязательных и необязательных полей

Вы можете объединить операции hasAll и hasOnly в правилах безопасности, чтобы одни поля были обязательными, а другие — разрешенными. Например, в этом примере требуется, чтобы все новые документы содержали поля name , location » и city , а также опционально разрешены поля address , hours и cuisine .

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.data с объектом resource.data , представляющим документ в базе данных до обновления. В результате будет создан объект mapDiff , содержащий все изменения между двумя разными картами.

Вызвав метод affectedKeys() для этого mapDiff, вы можете получить набор полей, изменённых при редактировании. Затем вы можете использовать функции, такие как hasOnly() или hasAny() чтобы убедиться, что этот набор содержит (или не содержит) определённые элементы.

Предотвращение изменения некоторых полей

Используя метод hasAny() для набора, сгенерированного affectedKeys() , а затем отрицая результат, вы можете отклонить любой клиентский запрос, который пытается изменить поля, которые вы не хотите изменять.

Например, вы можете разрешить клиентам обновлять информацию о ресторане, но не изменять их средний балл или количество отзывов.

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() чтобы указать список полей, которые вы хотите изменить. Обычно это считается более безопасным, поскольку запись в любые новые поля документа по умолчанию запрещена, пока вы явно не разрешите её в правилах безопасности.

Например, вместо того, чтобы запрещать поля average_score и rating_count , вы можете создать правила безопасности, которые разрешат клиентам изменять только поля name , location , city , address , hours и 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']));
    }
  }
}

Это означает, что если в какой-либо будущей итерации вашего приложения документы ресторана будут содержать поле telephone , попытки редактировать это поле будут безуспешными, пока вы не вернетесь и не добавите это поле в список hasOnly() в ваших правилах безопасности.

Обеспечение соблюдения типов полей

Ещё одним следствием отсутствия схемы в Cloud Firestore является отсутствие ограничений на уровне базы данных относительно того, какие типы данных могут храниться в определённых полях. Однако это можно реализовать в правилах безопасности с помощью оператора is .

Например, следующее правило безопасности требует, чтобы поле score обзора было целым числом, поля headline , content и author_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 : bool , bytes , float , int , list , latlng , number , path , map , string и timestamp . Оператор is также поддерживает типы данных constraint , duration , set и map_diff , но поскольку они генерируются самим языком правил безопасности, а не клиентами, они редко используются в большинстве практических приложений.

Типы данных list и map не поддерживают универсальные типы (generics) и аргументы типа. Другими словами, вы можете использовать правила безопасности, чтобы обеспечить, чтобы определённое поле содержало список или карту, но вы не можете обеспечить, чтобы поле содержало список всех целых чисел или всех строк.

Аналогичным образом, вы можете использовать правила безопасности для принудительного применения значений типов к определенным записям в списке или карте (используя обозначение скобок или ключевые имена соответственно), но не существует быстрого способа принудительного применения типов данных для всех элементов карты или списка одновременно.

Например, следующие правила гарантируют, что поле 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
      }
    }
  }
}

Обеспечение типов для необязательных полей

Важно помнить, что вызов request.resource.data.foo для документа, в котором foo отсутствует, приводит к ошибке, и, следовательно, любое правило безопасности, выполняющее этот вызов, отклонит запрос. Эту ситуацию можно решить с помощью метода get для request.resource.data . Метод 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 ).

Частичная запись никогда не допускается.

Последнее замечание о Cloud Firestore Security Rules : они либо разрешают клиенту вносить изменения в документ, либо отклоняют все изменения. Нельзя создать правила безопасности, которые разрешают запись в одни поля документа и запрещают запись в другие в рамках одной операции.