Условия использования в правилах безопасности базы данных в реальном времени

Данное руководство основано на руководстве по изучению основного языка правил безопасности Firebase и показывает, как добавлять условия в правила безопасности вашей базы данных Firebase Realtime Database.

Основным элементом правил безопасности баз данных в реальном времени является условие . Условие — это логическое выражение, определяющее, разрешена или запрещена конкретная операция. Для простых правил вполне подойдут литералы true и false в качестве условий. Но язык правил безопасности баз данных в реальном времени предоставляет способы написания более сложных условий, которые могут:

  • Проверка аутентификации пользователя
  • Сравните имеющиеся данные с вновь предоставленными.
  • Получайте доступ к различным частям вашей базы данных и сравнивайте их.
  • Проверка входящих данных
  • Используйте структуру входящих запросов для реализации логики безопасности.

Использование переменных $ для захвата сегментов пути

Вы можете захватывать части пути для чтения или записи, объявляя переменные захвата с префиксом $ . Это служит в качестве подстановочного знака и сохраняет значение этого ключа для использования в условиях правил:

{
  "rules": {
    "rooms": {
      // this rule applies to any child of /rooms/, the key for each room id
      // is stored inside $room_id variable for reference
      "$room_id": {
        "topic": {
          // the room's topic can be changed if the room id has "public" in it
          ".write": "$room_id.contains('public')"
        }
      }
    }
  }
}

Динамические переменные $ также можно использовать параллельно с постоянными именами путей. В этом примере мы используем переменную $other для объявления правила .validate , которое гарантирует, что widget нет дочерних элементов, кроме title и color . Любая запись, которая приведет к созданию дополнительных дочерних элементов, завершится ошибкой.

{
  "rules": {
    "widget": {
      // a widget can have a title or color attribute
      "title": { ".validate": true },
      "color": { ".validate": true },

      // but no other child paths are allowed
      // in this case, $other means any key excluding "title" and "color"
      "$other": { ".validate": false }
    }
  }
}

Аутентификация

Один из наиболее распространенных шаблонов правил безопасности — это управление доступом на основе состояния аутентификации пользователя. Например, ваше приложение может разрешить запись данных только авторизованным пользователям.

Если ваше приложение использует аутентификацию Firebase, переменная request.auth содержит информацию для аутентификации клиента, запрашивающего данные. Для получения дополнительной информации о request.auth см. справочную документацию .

Интеграция Firebase Authentication с Firebase Realtime Database позволяет управлять доступом к данным для каждого пользователя с помощью условий. После аутентификации пользователя переменная auth в правилах безопасности вашей базы данных Realtime Database будет заполнена информацией о пользователе. Эта информация включает в себя его уникальный идентификатор ( uid ), а также данные связанных учетных записей, такие как идентификатор Facebook или адрес электронной почты, и другую информацию. Если вы используете собственный поставщик аутентификации, вы можете добавить свои собственные поля в полезную нагрузку аутентификации пользователя.

В этом разделе объясняется, как объединить язык правил безопасности базы данных Firebase Realtime Database с информацией об аутентификации ваших пользователей. Объединив эти два понятия, вы можете контролировать доступ к данным на основе идентификации пользователя.

Переменная auth

Перед аутентификацией заданная в правилах переменная auth имеет значение null.

После аутентификации пользователя с помощью Firebase Authentication, он будет обладать следующими атрибутами:

поставщик Используемый метод аутентификации ("password", "anonymous", "facebook", "github", "google" или "twitter").
uid Уникальный идентификатор пользователя, гарантированно уникальный для всех провайдеров.
токен Содержимое токена Firebase Auth ID. Дополнительные сведения см. в справочной документации по auth.token .

Вот пример правила, использующего переменную auth для обеспечения возможности записи каждым пользователем только в указанный для него путь:

{
  "rules": {
    "users": {
      "$user_id": {
        // grants write access to the owner of this user account
        // whose uid must exactly match the key ($user_id)
        ".write": "$user_id === auth.uid"
      }
    }
  }
}

Структурирование базы данных для поддержки условий аутентификации

Обычно полезно структурировать базу данных таким образом, чтобы упростить написание Rules . Один из распространенных шаблонов хранения пользовательских данных в Realtime Database — это хранение всех пользователей в одном узле users , дочерними элементами которого являются значения uid для каждого пользователя. Если вы хотите ограничить доступ к этим данным таким образом, чтобы только авторизованный пользователь мог видеть свои собственные данные, ваши правила будут выглядеть примерно так.

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

Работа с пользовательскими утверждениями аутентификации

Для приложений, требующих индивидуального контроля доступа для разных пользователей, Firebase Authentication позволяет разработчикам устанавливать утверждения для пользователя Firebase . Эти утверждения доступны в переменной auth.token в ваших правилах. Вот пример правил, использующих пользовательское утверждение hasEmergencyTowel :

{
  "rules": {
    "frood": {
      // A towel is about the most massively useful thing an interstellar
      // hitchhiker can have
      ".read": "auth.token.hasEmergencyTowel === true"
    }
  }
}

Разработчики, создающие собственные пользовательские токены аутентификации, могут дополнительно добавлять к ним утверждения. Эти утверждения доступны в переменной auth.token в ваших правилах.

Существующие данные против новых данных

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

Для наглядности, это правило позволило бы нам создавать новые записи или удалять существующие, но не вносить изменения в уже имеющиеся ненулевые данные:

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

Ссылки на данные в других путях

В качестве критерия для правил можно использовать любые данные. Используя предопределенные переменные root , data и newData , мы можем получить доступ к любому пути так, как он существовал бы до или после события записи.

Рассмотрим следующий пример, который разрешает операции записи, если значение узла /allow_writes/ равно true , родительский узел не имеет установленного флага readOnly , и в записанных данных есть дочерний узел с именем foo :

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

Проверка данных

Проверка структуры данных и подтверждение формата и содержимого данных должны осуществляться с помощью правил ` .validate , которые выполняются только после успешного предоставления доступа правилом ` .write . Ниже приведен пример определения правила ` .validate , которое разрешает только даты в формате ГГГГ-ММ-ДД в период с 1900 по 2099 год, что проверяется с помощью регулярного выражения.

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"

Правила .validate — это единственный тип правил безопасности, которые не каскадируются. Если какое-либо правило проверки не выполняется для какой-либо дочерней записи, вся операция записи будет отклонена. Кроме того, определения правил проверки игнорируются при удалении данных (то есть, когда записываемое новое значение равно null ).

На первый взгляд это может показаться мелочью, но на самом деле это важные моменты для написания мощных правил безопасности для базы данных Firebase Realtime Database. Рассмотрим следующие правила:

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

Рассматривая этот вариант, взгляните на результаты следующих операций записи:

JavaScript
var ref = db.ref("/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Objective-C
Примечание: Этот продукт Firebase недоступен в целевом приложении App Clip.
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref child:@"size"] setValue: @99];
Быстрый
Примечание: Этот продукт Firebase недоступен в целевом приложении App Clip.
var ref = FIRDatabase.database().reference().child("widget")

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
ОТДЫХ
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Теперь рассмотрим ту же структуру, но с использованием правил .write вместо .validate :

{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

В этом варианте любая из следующих операций будет выполнена успешно:

JavaScript
var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Objective-C
Примечание: Этот продукт Firebase недоступен в целевом приложении App Clip.
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];
Быстрый
Примечание: Этот продукт Firebase недоступен в целевом приложении App Clip.
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
Java
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
ОТДЫХ
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Это иллюстрирует различия между правилами .write и .validate . Как показано, все эти правила следует писать с использованием .validate , за исключением, возможно, правила newData.hasChildren() , которое зависит от того, следует ли разрешать удаление.

Правила на основе запросов

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

Например, следующее правило, основанное на запросах, использует правила безопасности на основе пользователей и правила, основанные на запросах, для ограничения доступа к данным в коллекции baskets покупок только для тех корзин, которыми владеет активный пользователь:

"baskets": {
  ".read": "auth.uid !== null &&
            query.orderByChild === 'owner' &&
            query.equalTo === auth.uid" // restrict basket access to owner of basket
}

Следующий запрос, включающий параметры запроса в правило, будет выполнен успешно:

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

Однако запросы, не содержащие параметры правила, завершатся ошибкой PermissionDenied :

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

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

Например, следующее правило ограничивает доступ на чтение только первыми 1000 результатами запроса, упорядоченными по приоритету:

messages: {
  ".read": "query.orderByKey &&
            query.limitToFirst <= 1000"
}

// Example queries:

db.ref("messages").on("value", cb)                // Would fail with PermissionDenied

db.ref("messages").limitToFirst(1000)
                  .on("value", cb)                // Would succeed (default order by key)

Следующие выражения query. доступны в правилах безопасности баз данных в реальном времени.

Выражения правил на основе запросов
Выражение Тип Описание
query.orderByKey
query.orderByPriority
query.orderByValue
логический Значение true для запросов, упорядоченных по ключу, приоритету или значению. В противном случае — false.
query.orderByChild нить
нулевой
Для представления относительного пути к дочернему узлу используйте строку. Например, query.orderByChild === "address/zip" . Если запрос не упорядочен по дочернему узлу, это значение будет равно null.
запрос.начало
запрос.конецВ
query.equalTo
нить
число
логический
нулевой
Получает границы выполняемого запроса или возвращает null, если границы не заданы.
query.limitToFirst
query.limitToLast
число
нулевой
Получает лимит для выполняемого запроса или возвращает null, если лимит не установлен.

Следующие шаги

После обсуждения условий у вас сформировалось более глубокое понимание Rules , и вы готовы:

Узнайте, как обрабатывать основные сценарии использования, и освойте рабочий процесс разработки, тестирования и развертывания Rules :

Изучите особенности Rules , специфичные для Realtime Database :