Итак, вы, наконец, готовы снять тренировочные колеса с вашего проекта Flamelink и начать его жить. Прежде чем вы это сделаете, вы установили правильные правила для своей базы данных? Нет уж, стоит!

Если вы впервые слышите о Flamelink, CMS для Firebase, посетите наш веб-сайт, чтобы начать работу. После связывания вашего проекта Firebase с Flamelink вернитесь сюда, чтобы прочитать о защите вашего контента.

РЕДАКТИРОВАТЬ: обратите внимание, что в этой статье обсуждается установка правил безопасности для вашей базы данных Firebase Realtime, а не для Cloud Firestore. Дополнительная информация о CF в ближайшее время.

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

Теперь Flamelink не может работать, когда ваша БД находится в заблокированном режиме, потому что мы не сможем читать / писать в БД из вашего браузера. Единственный способ получить доступ к базе данных в заблокированном режиме - из серверной среды, для которой потребуется доступ через учетную запись службы. В Flamelink мы решили не идти по этому пути и предоставить вам, конечному пользователю, полный контроль над вашим проектом и уровнем доступа, который вы можете предоставить нам, пока спите по ночам. За это приходится платить с точки зрения удобного взаимодействия с пользователем, которое мы можем предложить, и в будущем мы можем предоставить оба варианта, но я отвлекся.

Чтобы быстро начать работу с Flamelink, мы предлагаем вам установить следующие правила базы данных для RTDB (базы данных реального времени):

{
  "rules": {
    "flamelink": {
      ".read": "auth != null",
      ".write": "auth != null",
      "users": {
        ".indexOn": ["email", "id"]
      }
    }
  }
}

На простом английском языке это гласит:

Нет доступа за пределами пространства имен «flamelink», НО доступ для чтения и записи аутентифицированным пользователям внутри пространства имен «flamelink».

Индекс пользователя в полях "электронная почта" и "идентификатор" просто предназначен для повышения производительности запросов и не важен для этой статьи об управлении доступом.

Это нормально для быстрого начала работы, но вы можете себе представить, что это не готовая к работе система безопасности, позволяющая любому аутентифицированному пользователю писать в вашу базу данных. С другой стороны, вы можете захотеть, чтобы часть контента была доступна для чтения всем, независимо от того, вошли они в систему или нет - подумайте о сообщениях в блогах на вашем веб-сайте и т. Д. Итак, как это можно улучшить? Давайте рассмотрим несколько вариантов.

Что нужно знать

Об установке правил безопасности для RTDB необходимо знать несколько вещей:

  1. Правила безопасности полностью игнорируются при доступе с сервера, они применяются только при доступе со стороны клиента - браузера.
  2. Если правило предоставляет доступ для чтения / записи к родительскому узлу, любые другие дочерние узлы, дополнительно вложенные в структуру БД, также будут иметь доступ. Другими словами, вы не можете установить для правила значение false, если оно уже истинно из правила, расположенного выше в структуре БД.

Посмотрите это видео, чтобы получить действительно хорошее представление о правилах безопасности RTDB, если вы еще не знакомы:

Доступ для чтения для вашего приложения или веб-сайта

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

{
  "rules": {
    "flamelink": {
      ".read": "auth != null",
      ".write": "auth != null",
      "users": {
        ".indexOn": ["email"]
      },
      "environments": {
        "$environment": {
          "content": {
            "nonSensitiveContentType": {
              ".read": true
            }
          }
          "schemas": {
            ".read": true
          }
        }
      }
    }
  }
}

Обратите внимание на свойство nonSensitiveContentType, которое можно заменить ключом определенного типа контента. Это относится к вашим данным, поэтому загляните в свою базу данных. Вы можете сделать это для любого количества типов контента. Если вы хотите, вы также можете сделать весь контент читаемым, просто установив:

"content": {
  ".read": true
}

Именно это мы сделали для схем в нашем примере. Если вы используете официальный Flamelink JavaScript SDK, вам нужно будет предоставить доступ для чтения к схемам, поскольку он используется для определения допустимости полей, реляционных и некоторых других преимуществ, таких как кеширование.

Другой вариант доступа для чтения для пользователей вашего приложения - по-прежнему требовать от пользователей аутентификации, но затем использовать анонимный вход в Firebase. Преимущество, которое это даст вам, заключается в том, что ваша БД будет доступна для чтения только из вашего приложения (или если вы разрешите аутентификацию для своего проекта), а не, например, через конечные точки REST.

Доступ для записи для определенных пользователей

Чтобы ограничить доступ для записи в вашу БД только пользователям Flamelink CMS, вы можете указать уникальные идентификаторы (uid) в своих правилах следующим образом:

{
  "rules": {
    "flamelink": {
      ".read": "auth != null",
      ".write": "auth.uid === '2TnyIXYi3FPeizykrJiLT972Oy53'",
      "users": {
        ".indexOn": ["email"]
      }
    }
  }
}

Вы можете найти UID для своих пользователей в разделе Аутентификация в консоли Firebase. Вы также можете очень легко указать несколько UID:

".write": "auth.uid === '2TnyIXYi3FPeizykrJiLT972Oy53' || auth.uid === 'LOkg1qVvLgTHWPyOkeBgrGaNuHy3'"

Если вы решили анонимно входить в систему для всех пользователей вашего приложения, вы можете дополнительно ограничить запись, проверив «анонимного» провайдера:

".write": "auth.provider !== 'anonymous'"

Очень динамичные правила

Я хочу начать с того, что мы не предлагаем вам делать это, но это возможно. Продолжить…

В Flamelink пользователям назначаются группы разрешений, каждая из которых имеет уникальный идентификатор. Эти группы разрешений соответствуют определенным разрешениям в приложении. Например, группу разрешений можно настроить так, чтобы разрешить только доступ просмотр для схем, но полный доступ CRUD для контента. Мы можем использовать эти группы разрешений для динамического ограничения доступа на уровне базы данных.

Открой меня, это может быть неприятно. Сначала мы рассмотрим, как мы можем применить разрешения «просмотр» к вашим типам контента, но тот же метод можно использовать для любых других действий CRUD.

{
  "rules": {
    "flamelink": {
      ".read": "auth != null",
      ".write": "auth != null",
      "environments": {
        "$environment": {
          "content": {
            "$contentType": {
              "$locale": {
                ".read": "auth != null && root.child('flamelink').child('permissions').child(root.child('flamelink').child('users').child(auth.uid).child('permissions').val() + '').child('content').child($environment).child($contentType).child('view').val() === true"
              }
            }
          }
        }
      }
    }
  }
}

Ух ты! Какого черта?! Хорошо, давайте разберемся с этим, потому что идея проста, а синтаксис - не особо. Обещаю, это будет иметь смысл.

Идея: получите группу разрешений пользователя и проверьте, настроена ли эта группа разрешений на разрешение «просмотра» определенного содержания.

Синтаксис: правило состоит из двух частей: получение идентификатора группы разрешений и последующая проверка конфигурации разрешений для этой группы.

root
  .child('flamelink')
  .child('users')
  .child(auth.uid)
  .child('permissions')
  .val() + ''

Этот код начинается с корня вашей базы данных и детализируется до flamelink.users.<uid>.permissions, где ‹uid› - это идентификатор пользователя, пытающегося получить доступ к БД. Значение этого поля базы данных является целым числом, поэтому мы преобразуем его в строку с + '', чтобы мы могли использовать его в следующей части нашего правила.

root
  .child('flamelink')
  .child('permissions')
  .child(<our-previous-query>)
  .child('content')
  .child($environment)
  .child($contentType)
  .child('view')
  .val() === true

Опять же, мы начинаем с корня БД и углубляемся, пока не дойдем до фактической конфигурации группы разрешений: flamelink.permissions.<user-permission-group>.content.<environment>.<content-type>.view.

Каждая конфигурация группы разрешений состоит из следующих 4 логических свойств, которые соответствуют стандартной конфигурации CRUD:

{
  create: true,
  delete: false,
  update: true,
  view: true
}

Чтобы проверить наличие других разрешений, просто замените «просмотр» на «обновить», «удалить» или «создать ».

Вы также могли заметить часть auth != null в начале правила. Это необходимо для того, чтобы мы по-прежнему проверяли, что пользователь вошел в систему, иначе вся наша тяжелая работа будет отменена кем-то, кто просто не вошел в систему.

Это все для правила ".read". Правило ".write" похоже на наши чтения, но более сложное, потому что нам также нужно учитывать, что пользователь пытается сделать с данными, чтобы определить, следует ли нам проверять создать, обновить или удалить конфигурацию.

Мы смелые разработчики, так что продолжим.

{
    ".write": "auth !== null &&
    ((!data.exists() &&
      root
        .child('flamelink')
        .child('permissions')
        .child(
          root
            .child('flamelink')
            .child('users')
            .child(auth.uid)
            .child('permissions')
            .val() + ''
        )
        .child('content')
        .child($environment)
        .child($contentType)
        .child('create')
        .val() === true) ||
      (!newData.exists() &&
        root
          .child('flamelink')
          .child('permissions')
          .child(
            root
              .child('flamelink')
              .child('users')
              .child(auth.uid)
              .child('permissions')
              .val() + ''
          )
          .child('content')
          .child($environment)
          .child($contentType)
          .child('delete')
          .val() === true) ||
      (data.exists() && newData.exists() &&
        root
          .child('flamelink')
          .child('permissions')
          .child(
            root
              .child('flamelink')
              .child('users')
              .child(auth.uid)
              .child('permissions')
              .val()
          )
          .child('content')
          .child($environment)
          .child($contentType)
          .child('update')
          .val() === true))"
  }

Теперь, когда мы сорвали повязку, что здесь происходит?

Помимо auth != null проверки зарегистрированных пользователей, в нашем правиле есть 3 отдельные части, каждая из которых имеет дело с разными действиями (создание, удаление и обновление).

Для нашего действия create мы используем data.exist() метод Firebase, чтобы проверить, нет ли в настоящее время данных для определенного содержания. Так мы узнаем, что кто-то пытается добавить новые данные.

Для нашего действия удаления мы используем метод newData.exists(), чтобы проверить, не существуют ли новые данные. Если действие пользователя не приведет к появлению новых данных, мы знаем, что он пытается что-то удалить.

Для нашего последнего действия обновления мы объединяем методы data.exists() и newData.exists(), чтобы определить, что пользователь пытается изменить существующие данные на что-то другое.

Это было не так уж плохо, правда?

Полный пример того, как это можно применить, см. В этой сути.

Этот подход не лишен ограничений. Поскольку Flamelink является вечнозеленым и постоянно развивающимся продуктом, постоянно добавляются новые функции, что может привести к добавлению новых узлов в базу данных. Если вы привяжете базу данных так сильно, что мы не сможем внести необходимые обновления в структуру вашей базы данных, у вас не будет доступа к блестящим новым функциям. Вы можете обойти это, объединив конкретное правило UID, которое мы рассмотрели ранее, с этой динамической настройкой и убедитесь, что если пользователь, вошедший в систему, является владельцем проекта, любые записи могут быть сделаны в базу данных. Это обеспечит применение необходимых изменений структуры БД при развертывании новых функций и входе владельца в проект.

При этом мы очень редко вносим структурные изменения из-за вечнозеленой природы продукта.

Пользовательские утверждения Firebase

Лучшее мы оставили напоследок. Наиболее красноречивое решение - использовать менее известную функцию Firebase: Custom Claims. Мы хотели бы поставлять Flamelink с настраиваемыми претензиями из коробки, но таможенные претензии могут быть установлены только из привилегированной серверной среды с помощью Firebase Admin SDK. Это означает, что вы, как владелец проекта, должны будете справиться с этим самостоятельно.

Что такое особые претензии?

Проще говоря, настраиваемые утверждения - это настраиваемые атрибуты, установленные для учетных записей пользователей. Вы можете, например, установить для пользователя атрибут isAdmin. Это очень мощный инструмент, поскольку он дает возможность реализовывать различные стратегии управления доступом, включая управление доступом на основе ролей, в приложениях Firebase. Удивительно то, что эти настраиваемые атрибуты можно использовать в правилах безопасности вашей базы данных.

Некоторые идеи о том, как их можно использовать

Настраиваемые утверждения следует использовать только для управления доступом, а не для хранения каких-либо дополнительных пользовательских данных. Лучше всего хранить дополнительные данные в своей базе данных.

При настройке пользовательских утверждений вы можете упростить задачу и установить атрибут flamelinkUser для всех пользователей Firebase, которые должны иметь доступ на запись к контенту. В качестве альтернативы вы можете установить такие сложные утверждения, которые захотите, но помните, что полезная нагрузка настраиваемых утверждений не должна превышать предел в 1000 байт. Рекомендуется сделать его как можно меньшим, поскольку эти утверждения отправляются вместе со всеми сетевыми запросами, и большая полезная нагрузка может отрицательно сказаться на производительности.

Как использовать эти настраиваемые утверждения в наших правилах безопасности?

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

{
  "rules": {
    "flamelink": {
      ".read": "auth != null",
      ".write": "auth.token.flamelinkUser === true"
    }
  }
}

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

Единственное требование для настройки пользовательских утверждений - это то, что они устанавливаются из серверной среды с помощью Firebase Admin SDK, будь то с автономным сервером Express, который у вас запущен, или с использованием Облачных функций для Firebase, это зависит от вас. Код выглядит примерно так (в примере используется JavaScript, но вы можете использовать любой из поддерживаемых языков на стороне сервера):

// import admin SDK
const admin = require('firebase-admin');
// initialize admin app with any of the supported options
admin.initializeApp(/* config here */);
// create your custom claims object (whatever you want)
const customClaims = {
  flamelinkUser: true
};
// set the custom claims object for given UID
admin.auth().setCustomUserClaims(user.uid, customClaims)

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

Заключение

Надеюсь, это дало вам представление о том, насколько эффективны и гибки правила безопасности Firebase. Я рекомендую вам прочитать больше об этих правилах в документации Firebase.

Если у вас есть какие-либо другие идеи о том, как мы можем улучшить эти правила безопасности, сообщите нам об этом в комментариях ниже или присоединитесь к нашему Сообществу Slack, мы будем рады видеть вас.