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

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

Основным строительным блоком правил безопасности базы данных в реальном времени является условие . Условие — это логическое выражение, которое определяет, следует ли разрешить или запретить определенную операцию. Для базовых правил использование литералов 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 Authentication, переменная request.auth содержит информацию об аутентификации для клиента, запрашивающего данные. Для получения дополнительной информации о request.auth см. справочную документацию .

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

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

Переменная auth

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

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

поставщик Используемый метод аутентификации («пароль», «анонимный», «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 , которое допускает только даты в формате YYYY-MM-DD между 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. Рассмотрите следующие правила:

{
  "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);
Ява
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)
Ява
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. доступны в правилах безопасности базы данных в реальном времени.

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

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

После этого обсуждения условий вы получите более глубокое понимание Rules и будете готовы:

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

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