Используйте условия в правилах безопасности Realtime Database

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

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

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

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

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

{
  "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 Authentication, переменная 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"
      }
    }
  }
}

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

Обычно полезно структурировать базу данных так, чтобы упростить запись Security 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 – единственный тип правил безопасности, которые не применяются каскадно. Если хотя бы одно правило проверки не будет выполнено для какой-либо дочерней записи, вся операция записи будет отклонена. Кроме того, определения 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 недоступна для блиц-приложений.
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];
Swift
Примечание. Эта функция Firebase недоступна для блиц-приложений.
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);
REST
# 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 недоступна для блиц-приложений.
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];
Swift
Примечание. Эта функция Firebase недоступна для блиц-приложений.
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);
REST
# 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)

В правилах безопасности Realtime Database доступны следующие выражения query..

Выражения правил на основе запросов
Выражение Тип Описание
query.orderByKey
query.orderByPriority
query.orderByValue
Логическое значение True для запросов, упорядоченных по ключу, приоритету или значению. В противном случае значение равно False.
query.orderByChild string
null
Используйте строку, чтобы указать относительный путь к дочернему узлу. Пример: query.orderByChild === "address/zip". Если запрос не упорядочен по дочернему узлу, это значение равно null.
query.startAt
query.endAt
query.equalTo
string
number
boolean
null
Возвращает границы выполняемого запроса или null, если границы не заданы.
query.limitToFirst
query.limitToLast
number
null
Получает ограничение на выполняемый запрос или возвращает значение null, если ограничение не задано.

Дальнейшие действия

Теперь вы лучше понимаете, как работают условия в Security Rules, и можете:

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

Узнайте о функциях Security Rules, которые доступны только в Realtime Database: