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

На этой странице, опираясь на концепции, изложенные в разделах «Структурирование правил безопасности» и «Написание условий для правил безопасности» , объясняется, как использовать 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 не поддерживают обобщения или аргументы типа. Другими словами, вы можете использовать правила безопасности, чтобы гарантировать, что определенное поле содержит список или карту, но вы не можете гарантировать, что поле содержит список только целых чисел или только строк.

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

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