Firebase SQL Connect обеспечивает надежную безопасность на стороне клиента благодаря следующим функциям:
- Авторизация мобильных и веб-клиентов
- Индивидуальные средства управления авторизацией на уровне запросов и изменений
- Аттестация приложений с помощью Firebase App Check .
SQL Connect расширяет возможности этой системы безопасности за счет следующих функций:
- Авторизация на стороне сервера
- Безопасность пользователей Firebase и Cloud SQL с помощью IAM.
Авторизация запросов и изменений данных клиента.
SQL Connect полностью интегрирован с Firebase Authentication , поэтому вы можете использовать подробные данные о пользователях, получающих доступ к вашим данным (аутентификация), при проектировании системы, определяющей, к каким данным эти пользователи имеют доступ (авторизация).
SQL Connect предоставляет директиву @auth для запросов и мутаций, которая позволяет установить уровень аутентификации, необходимый для авторизации операции. В этом руководстве представлено описание директивы @auth с примерами .
Кроме того, SQL Connect поддерживает выполнение запросов, встроенных в мутации, поэтому вы можете получить дополнительные критерии авторизации, которые вы сохранили в своей базе данных, и использовать эти критерии в директивах @check , чтобы решить, разрешены ли содержащиеся в них мутации. В этом случае авторизации директива @redact позволяет управлять тем, возвращаются ли результаты запроса клиентам по протоколу передачи, а встроенный запрос опускается в сгенерированных SDK. Введение в эти директивы с примерами можно найти здесь.
Разберитесь в директиве @auth
Вы можете параметризовать директиву @auth , чтобы она соответствовала одному из нескольких предустановленных уровней доступа, охватывающих множество распространенных сценариев доступа. Эти уровни варьируются от PUBLIC (разрешает запросы и изменения данных от всех клиентов без какой-либо аутентификации) до NO_ACCESS (запрещает запросы и изменения данных за пределами привилегированных серверных сред с использованием Firebase Admin SDK ). Каждый из этих уровней связан с потоками аутентификации, предоставляемыми Firebase Authentication .
| Уровень | Определение |
|---|---|
PUBLIC | Операцию может выполнить любой пользователь, с аутентификацией или без неё. |
USER_ANON | Любой идентифицированный пользователь, включая тех, кто вошел в систему анонимно с помощью Firebase Authentication , имеет право выполнять запрос или мутацию. |
USER | Любой пользователь, вошедший в систему с использованием Firebase Authentication имеет право выполнять запрос или мутацию, за исключением анонимных пользователей. |
USER_EMAIL_VERIFIED | Любой пользователь, вошедший в систему с помощью Firebase Authentication и подтвердивший свой адрес электронной почты, имеет право выполнять запрос или мутацию. |
NO_ACCESS | Данная операция не может быть выполнена вне контекста Admin SDK. |
Используя эти предустановленные уровни доступа в качестве отправной точки, вы можете определить сложные и надежные проверки авторизации в директиве @auth , используя фильтры where и выражения Common Expression Language (CEL), оцениваемые на сервере.
Используйте директиву @auth для реализации распространенных сценариев авторизации.
Заданные уровни доступа являются отправной точкой для авторизации.
Уровень доступа USER — это наиболее полезный базовый уровень, с которого можно начать.
Полностью защищенный доступ будет основан на уровне USER , а также на фильтрах и выражениях, проверяющих атрибуты пользователя, атрибуты ресурсов, роли и другие параметры. Уровни USER_ANON и USER_EMAIL_VERIFIED являются вариантами уровня USER .
Синтаксис выражений позволяет оценивать данные с помощью объекта auth , представляющего данные аутентификации, передаваемые вместе с операциями, как стандартные данные в токенах аутентификации, так и пользовательские данные в токенах. Список полей, доступных в объекте auth , см. в справочном разделе .
Конечно, существуют сценарии, когда уровень доступа PUBLIC является правильным начальным уровнем. Однако, уровень доступа всегда является отправной точкой, и для обеспечения надежной безопасности необходимы дополнительные фильтры и выражения.
В этом руководстве теперь приведены примеры того, как создавать приложения на основе USER и PUBLIC .
Показательный пример
Приведенные ниже примеры передовой практики относятся к следующей схеме для платформы ведения блогов, где определенный контент доступен только по платному тарифному плану.
Подобная платформа, скорее всего, будет моделировать Users и Posts .
type User @table(key: "uid") {
uid: String!
name: String
birthday: Date
createdAt: Timestamp! @default(expr: "request.time")
}
type Post @table {
author: User!
text: String!
# "one of 'draft', 'public', or 'pro'"
visibility: String! @default(value: "draft")
# "the time at which the post should be considered published. defaults to
# immediately"
publishedAt: Timestamp! @default(expr: "request.time")
createdAt: Timestamp! @default(expr: "request.time")
updatedAt: Timestamp! @default(expr: "request.time")
}
Ресурсы, принадлежащие пользователям
Firebase рекомендует писать фильтры и выражения, проверяющие принадлежность ресурса пользователю, в следующих случаях — принадлежность объекта Posts .
В следующих примерах данные из токенов аутентификации считываются и сравниваются с помощью выражений. Типичный шаблон — использование выражений типа where: {authorUid: {eq_expr: "auth.uid"}} для сравнения сохраненного authorUid с auth.uid (идентификатором пользователя), переданным в токене аутентификации.
Создавать
Данная процедура авторизации начинается с добавления значения auth.uid из токена авторизации к каждой новой Post в качестве поля authorUid , что позволяет сравнивать данные в последующих проверках авторизации.
# Create a new post as the current user
mutation CreatePost($text: String!, $visibility: String) @auth(level: USER) {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Обновлять
Когда клиент пытается обновить Post , вы можете проверить переданный auth.uid на соответствие сохраненному authorUid .
# Update one of the current user's posts
mutation UpdatePost($id: UUID!, $text: String, $visibility: String) @auth(level:USER) {
post_update(
# only update posts whose author is the current user
first: { where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}
}}
data: {
text: $text
visibility: $visibility
# insert the current server time for updatedAt
updatedAt_expr: "request.time"
}
)
}
Удалить
Аналогичный метод используется для авторизации операций удаления.
# Delete one of the current user's posts
mutation DeletePost($id: UUID!) @auth(level: USER) {
post_delete(
# only delete posts whose author is the current user
first: { where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}
}}
)
}
# Common display information for a post
fragment DisplayPost on Post {
id, text, createdAt, updatedAt
author { uid, name }
}
Список
# List all posts belonging to the current user
query ListMyPosts @auth(level: USER) {
posts(where: {
userUid: {eq_expr: "auth.uid"}
}) {
# See the fragment above
...DisplayPost
# also show visibility since it is user-controlled
visibility
}
}
Получать
# Get a post only if it belongs to the current user
query GetMyPost($id: UUID!) @auth(level: USER) {
post(key: {id: $id},
first: {where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}}
}}, {
# See the fragment above
...DisplayPost
# also show visibility since it is user-controlled
visibility
}
}
Фильтрация данных
Система авторизации SQL Connect позволяет создавать сложные фильтры в сочетании с предустановленными уровнями доступа, такими как PUBLIC , а также с использованием данных из токенов авторизации.
Система авторизации также позволяет использовать только выражения, без указания базового уровня доступа, как показано в некоторых из следующих примеров.
Фильтрация по атрибутам ресурса
В данном случае авторизация не основана на токенах аутентификации, поскольку базовый уровень безопасности установлен на PUBLIC . Однако мы можем явно указать в нашей базе данных, что доступ к ней разрешен для всех пользователей; предположим, у нас есть записи Post в базе данных с уровнем visibility "public".
# List all posts marked as 'public' visibility
query ListPublicPosts @auth(level: PUBLIC) {
posts(where: {
# Test that visibility is "public"
visibility: {eq: "public"}
# Only display articles that are already published
publishedAt: {lt_expr: "request.time"}
}) {
# see the fragment above
...DisplayPost
}
}
Фильтрация по утверждениям пользователей
Предположим, вы настроили пользовательские утверждения, которые передают токены аутентификации для идентификации пользователей, использующих тарифный план «Pro» для вашего приложения, помеченный полем auth.token.plan в токене аутентификации. Ваши выражения могут проверять это поле.
# List all public or pro posts, only permitted if user has "pro" plan claim
query ProListPosts @auth(expr: "auth.token.plan == 'pro'") {
posts(where: {
# display both public posts and "pro" posts
visibility: {in: ['public', 'pro']},
# only display articles that are already published
publishedAt: {lt_expr: "request.time"},
}) {
# see the fragment above
...DisplayPost
# show visibility so pro users can see which posts are pro\
visibility
}
}
Фильтрация по порядку + лимиту
Или, например, вы могли установить visibility записей в Post таким образом, чтобы они соответствовали контенту, доступному для пользователей с платными правами, но для предварительного просмотра или ознакомительного списка данных можно дополнительно ограничить количество возвращаемых записей.
# Show 2 oldest Pro post as a preview
query ProTeaser @auth(level: USER) {
posts(
where: {
# show only pro posts
visibility: {eq: "pro"}
# that have already been published more than 30 days ago
publishedAt: {lt_time: {now: true, sub: {days: 30}}}
},
# order by publish time
orderBy: [{publishedAt: DESC}],
# only return two posts
limit: 2
) {
# See the fragment above
...DisplayPost
}
}
Фильтр по роли
Если в вашем пользовательском запросе определена роль admin , вы можете соответствующим образом тестировать и авторизовывать операции.
# List all posts unconditionally iff the current user has an admin claim
query AdminListPosts @auth(expr: "auth.token.admin == true") {
posts { ...DisplayPost }
}
Добавьте директивы @check и @redact для поиска данных авторизации.
Один из распространенных сценариев использования авторизации включает хранение пользовательских ролей авторизации в базе данных, например, в специальной таблице разрешений, и использование этих ролей для авторизации изменений, таких как создание, обновление или удаление данных.
Используя поиск данных авторизации, вы можете запрашивать роли на основе идентификатора пользователя и использовать выражения CEL для определения того, разрешена ли мутация. Например, вы можете написать мутацию UpdateMovieTitle , которая позволит авторизованному клиенту обновлять названия фильмов.
В дальнейшем обсуждении будем исходить из того, что база данных приложения для отзывов о фильмах хранит роль авторизации в таблице MoviePermission .
# MoviePermission
# Suppose a user has an authorization role with respect to records in the Movie table
type MoviePermission @table(key: ["movie", "user"]) {
movie: Movie! # implies another field: movieId: UUID!
user: User!
role: String!
}
Применение при мутациях
В приведенном ниже примере реализации мутация UpdateMovieTitle включает поле query для получения данных из MoviePermission , а также следующие директивы для обеспечения безопасности и надежности операции:
- Директива
@transaction, гарантирующая атомарное завершение или сбой всех запросов и проверок авторизации. - Директива
@redactисключает результаты запроса из ответа, что означает, что проверка авторизации выполняется на сервере SQL Connect но конфиденциальные данные не передаются клиенту. Пара директив
@checkдля оценки логики авторизации в результатах запроса, например, для проверки того, имеет ли данный userID соответствующую роль для внесения изменений.
mutation UpdateMovieTitle($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query @redact {
moviePermission( # Look up a join table called MoviePermission with a compound key.
key: {movieId: $movieId, userId_expr: "auth.uid"}
# Step 1a: Use @check to test if the user has any role associated with the movie
# Here the `this` binding refers the lookup result, i.e. a MoviePermission object or null
# The `this != null` expression could be omitted since rejecting on null is default behavior
) @check(expr: "this != null", message: "You do not have access to this movie") {
# Step 1b: Check if the user has the editor role for the movie
# Next we execute another @check; now `this` refers to the contents of the `role` field
role @check(expr: "this == 'editor'", message: "You must be an editor of this movie to update title")
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
Используйте в запросах
Поиск данных авторизации также полезен для ограничения запросов на основе ролей или других ограничений.
В следующем примере, который также использует схему MoviePermission , запрос проверяет, имеет ли запрашивающий соответствующую роль "администратора", позволяющую просматривать пользователей, которые могут редактировать фильм.
query GetMovieEditors($movieId: UUID!) @auth(level: PUBLIC) {
moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
}
moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
user {
id
username
}
}
}
Антишаблоны, которых следует избегать при авторизации.
В предыдущем разделе были рассмотрены шаблоны, которым следует следовать при использовании директивы @auth .
Также следует помнить о важных антипаттернах, которых следует избегать.
Избегайте передачи идентификаторов атрибутов пользователя и параметров токена аутентификации в аргументах запросов и мутаций.
Firebase Authentication — это мощный инструмент для представления потоков аутентификации и безопасного сбора данных аутентификации, таких как зарегистрированные идентификаторы пользователей и многочисленные поля, хранящиеся в токенах аутентификации.
Не рекомендуется передавать идентификаторы пользователей и данные токенов аутентификации в аргументах запросов и мутаций.
# Antipattern!
# This incorrectly allows any user to view any other user's posts
query AllMyPosts($userId: String!) @auth(level: USER) {
posts(where: {authorUid: {eq: $userId}}) {
id, text, createdAt
}
}
Избегайте использования уровня доступа USER без каких-либо фильтров.
Как уже неоднократно обсуждалось в руководстве, основные уровни доступа, такие как USER , USER_ANON , USER_EMAIL_VERIFIED являются базовыми и отправными точками для проверок авторизации, которые можно расширить с помощью фильтров и выражений. Использование этих уровней без соответствующего фильтра или выражения, проверяющего, какой пользователь выполняет запрос, по сути, эквивалентно использованию уровня PUBLIC .
# Antipattern!
# This incorrectly allows any user to view all documents
query ListDocuments @auth(level: USER) {
documents {
id
title
text
}
}
Избегайте использования уровней доступа PUBLIC или USER при прототипировании.
Для ускорения разработки может возникнуть соблазн установить для всех операций уровень доступа PUBLIC или USER без дополнительных улучшений, которые бы авторизовали все операции и позволили быстро протестировать код.
После проведения первоначального прототипирования таким способом начните переходить от NO_ACCESS к готовой к использованию в производственной среде авторизации с уровнями PUBLIC и USER . Однако не развертывайте их как PUBLIC или USER без добавления дополнительной логики, как показано в этом руководстве.
# Antipattern!
# This incorrectly allows anyone to delete any post
mutation DeletePost($id: UUID!) @auth(level: PUBLIC) {
post: post_delete(
id: $id,
)
}
Избегайте авторизации на основе непроверенных адресов электронной почты.
Предоставление доступа пользователям в рамках определенного домена — отличный способ ограничить доступ. Однако любой может заявить о владении электронной почтой при входе в систему. Убедитесь, что вы предоставляете доступ только тем адресам электронной почты, которые были подтверждены с помощью аутентификации Firebase.
# Antipattern!
# Anyone can claim an email address during sign-in
mutation CreatePost($text: String!, $visibility: String) @auth(expr: "auth.token.email.endsWith('@example.com')") {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Также проверьте auth.token.email_verified
mutation CreatePost($text: String!, $visibility: String) @auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@example.com')") {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Проверка авторизации с помощью Firebase CLI
Как указывалось ранее, предустановленные уровни доступа, такие как PUBLIC и USER являются отправной точкой для надежной авторизации и должны использоваться с дополнительными проверками авторизации на основе фильтров и выражений. Их не следует использовать отдельно без тщательного анализа сценария использования.
SQL Connect помогает вам провести аудит вашей стратегии авторизации, анализируя код коннектора при развертывании на сервере с помощью firebase deploy из Firebase CLI. Вы можете использовать этот аудит для проверки вашего кода.
При развертывании коннекторов интерфейс командной строки выведет результаты оценки существующего, измененного и нового кода операций в вашем коннекторе.
При изменении и создании новых операций интерфейс командной строки выдает предупреждения и запрашивает подтверждение при использовании определенных уровней доступа в новых операциях или при изменении существующих операций для использования этих уровней доступа.
Предупреждения и подсказки всегда появляются в следующих случаях:
-
PUBLIC
Кроме того, предупреждения и запросы появляются на следующих уровнях доступа, если вы не дополняете их фильтрами с использованием auth.uid :
-
USER -
USER_ANON -
USER_EMAIL_VERIFIED
Подавлять предупреждения о небезопасных операциях с помощью аргумента @auth(insecureReason:)
Во многих случаях вы придете к выводу, что использование уровней доступа PUBLIC и USER* вполне уместно.
Если ваш коннектор содержит множество операций, вам может потребоваться более понятный и актуальный результат аудита безопасности, исключающий операции, которые обычно вызывают предупреждение, но для которых у вас точно есть необходимый уровень доступа.
Вы можете подавить предупреждения для таких операций с помощью @auth(insecureReason:) . Например:
query listItem @auth(level: PUBLIC, insecureReason: "This operation is safe to expose to the public.")
{
items {
id name
}
}
Используйте Firebase App Check для аттестации приложений.
Аутентификация и авторизация являются важнейшими компонентами безопасности SQL Connect . Сочетание аутентификации и авторизации с аттестацией приложений обеспечивает очень надежное решение для обеспечения безопасности.
Благодаря аттестации через Firebase App Check , устройства, на которых работает ваше приложение, будут использовать поставщика аттестации приложений или устройств, который подтверждает, что операции SQL Connect исходят от вашего подлинного приложения, а запросы — от подлинного, не подвергавшегося изменениям устройства. Эта аттестация прикрепляется к каждому запросу, который ваше приложение отправляет в SQL Connect .
Чтобы узнать, как включить App Check для SQL Connect и интегрировать клиентский SDK в ваше приложение, ознакомьтесь с обзором App Check .
Уровни аутентификации для директивы @auth(level)
В таблице ниже перечислены все стандартные уровни доступа и их эквиваленты в системе CEL. Уровни аутентификации указаны от широкого к узкому — каждый уровень включает всех пользователей, соответствующих следующим уровням.
| Уровень | Определение |
|---|---|
PUBLIC | Операцию может выполнить любой пользователь, с аутентификацией или без неё. Важные замечания: Данные могут быть прочитаны или изменены любым пользователем. Firebase рекомендует такой уровень авторизации для общедоступных данных, таких как списки товаров или медиафайлов. См. примеры передовой практики и альтернативные варианты . Эквивалентно @auth(expr: "true")Фильтры и выражения @auth нельзя использовать в сочетании с этим уровнем доступа. Любые подобные выражения приведут к ошибке 400 (bad request). |
USER_ANON | Любой идентифицированный пользователь, включая тех, кто вошел в систему анонимно с помощью Firebase Authentication , имеет право выполнять запрос или мутацию. Примечание: USER_ANON является надмножеством USER .Важные замечания: Обратите внимание, что для этого уровня авторизации необходимо тщательно продумать запросы и мутации. Этот уровень позволяет пользователю входить в систему анонимно (автоматический вход, привязанный только к устройству пользователя) с использованием Authentication и сам по себе не выполняет других проверок, например, принадлежности данных пользователю. См. примеры передовой практики и альтернативные варианты . Поскольку в процессах анонимной Authentication используется идентификатор пользователя uid , уровень USER_ANON эквивалентен@auth(expr: "auth.uid != nil") |
USER | Любой пользователь, вошедший в систему с использованием Firebase Authentication имеет право выполнять запрос или мутацию, за исключением анонимных пользователей. Важные замечания: Обратите внимание, что запросы и мутации необходимо тщательно разрабатывать для этого уровня авторизации. Этот уровень проверяет только то, что пользователь авторизован с помощью Authentication , и сам по себе не выполняет других проверок, например, принадлежит ли информация пользователю. См. примеры передовой практики и альтернативные варианты . Эквивалентно @auth(expr: "auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'")" |
USER_EMAIL_VERIFIED | Любой пользователь, вошедший в систему с помощью Firebase Authentication и подтвердивший свой адрес электронной почты, имеет право выполнять запрос или мутацию. Особенности: Поскольку проверка электронной почты выполняется с помощью Authentication , она основана на более надежном методе Authentication , поэтому этот уровень обеспечивает дополнительную безопасность по сравнению с USER или USER_ANON . Этот уровень проверяет только то, что пользователь вошел в систему с помощью Authentication с подтвержденным адресом электронной почты, и сам по себе не выполняет других проверок, например, принадлежит ли информация пользователю. См. примеры передовой практики и альтернативы .Эквивалентно @auth(expr: "auth.uid != nil && auth.token.email_verified")" |
NO_ACCESS | Данная операция не может быть выполнена вне контекста Admin SDK. Эквивалентно @auth(expr: "false") |
Справочник CEL для @auth(expr)
Как показано в примерах в других разделах этого руководства, вы можете и должны использовать выражения, определенные в Common Expression Language (CEL), для управления авторизацией в SQL Connect с помощью директив @auth(expr:) и @check .
В этом разделе рассматривается синтаксис CEL, необходимый для создания выражений для этих директив.
Полная справочная информация по CEL представлена в спецификации CEL .
Тестовые переменные передаются в запросах и мутациях.
Синтаксис @auth(expr) позволяет получать доступ к переменным из запросов и мутаций и проверять их.
Например, вы можете включить переменную операции, такую как $status , используя vars.status .
mutation Update($id: UUID!, $status: Any) @auth(expr: "has(vars.status)")
Данные, доступные для выражений: request, response, this
Вы используете данные для:
- Оценка с использованием выражений CEL в директивах
@auth(expr:)и@check(expr:) - Присваивание с использованием серверных выражений,
<field>_expr.
Выражения CEL @auth(expr:) , @check(expr:) , @refresh(onMutationExecuted:) могут оценивать следующие значения:
-
request.operationName -
vars(псевдоним дляrequest.variables) -
auth(псевдоним дляrequest.auth)
При изменении типа данных вы можете получить доступ к содержимому следующих элементов и присвоить его:
-
response(для проверки частичных результатов в многошаговой логике)
Кроме того, выражения @check(expr:) могут оцениваться следующим образом:
-
this(значение текущего поля) -
response(для проверки частичных результатов в многошаговой логике)
А выражения @refresh(onMutationExecuted:) имеют следующую привязку:
-
mutation.variables -
mutation.auth
Привязка request.operationName
В привязке request.operarationName хранится тип операции: запрос или изменение.
Привязка vars (request.vars)
Функция привязки vars позволяет вашим выражениям получать доступ ко всем переменным, переданным в вашем запросе или мутации.
Вы можете использовать vars.<variablename> в выражении в качестве псевдонима для полного request.variables.<variablename> `.
# The following are equivalent
mutation StringType($v: String!) @auth(expr: "vars.v == 'hello'")
mutation StringType($v: String!) @auth(expr: "request.variables.v == 'hello'")
Привязка auth (request.auth)
Authentication идентифицирует пользователей, запрашивающих доступ к вашим данным, и предоставляет эту информацию в качестве привязки, на основе которой вы можете строить свои выражения.
В фильтрах и выражениях вы можете использовать auth в качестве псевдонима для request.auth .
В привязке аутентификации содержится следующая информация:
-
uid: Уникальный идентификатор пользователя, присваиваемый пользователю, отправившему запрос. -
token: Карта значений, собранных в процессе Authentication .
Для получения более подробной информации о содержимом файла auth.token см. раздел «Данные в токенах аутентификации».
response связывания
В привязке response содержатся данные, которые сервер собирает в ответ на запрос или изменение , по мере того, как эти данные собираются .
По мере выполнения операции, по мере успешного завершения каждого этапа, response содержатся данные об успешно завершенных этапах.
Структура привязки response определяется формой связанной с ним операции, включая (множественные) вложенные поля и (при необходимости) встроенные запросы.
Обратите внимание, что при доступе к данным ответа на встроенный запрос поля могут содержать любые типы данных в зависимости от данных, запрошенных во встроенном запросе; при доступе к данным, возвращаемым полями мутаций, такими как _insert и _delete , они могут содержать ключи UUID, количество удалений, значения NULL (см. справочник по мутациям ).
Например:
- В мутации, содержащей встроенный запрос, привязка
responseсодержит данные для поиска по адресуresponse.query.<fieldName>.<fieldName>...., в данном случаеresponse.query.todoListиresponse.query.todoList.priority.
mutation CheckTodoPriority(
$uniqueListName: String!
) {
# This query is identified as `response.query`
query @check(expr: "response.query.todoList.priority == 'high'", message: "This list is not for high priority items!") {
# This field is identified as `response.query.todoList`
todoList(where: { name: $uniqueListName }) {
# This field is identified as `response.query.todoList.priority`
priority
}
}
}
- В многошаговой мутации, например, с несколькими полями
_insert, привязкаresponseсодержит частичные данные поresponse.<fieldName>.<fieldName>...., в данном случае,response.todoList_insert.id.
mutation CreateTodoListWithFirstItem(
$listName: String!,
$itemContent: String!
) @transaction {
# Step 1
todoList_insert(data: {
id_expr: "uuidV4()",
name: $listName,
})
# Step 2:
todo_insert(data: {
listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
content: $itemContent,
})
}
this переплет
Привязка this выполняет директива @check связана с полем, к которому она прикреплена. В простом случае вы можете оценивать результаты запроса, содержащие одно значение.
mutation UpdateMovieTitle (
$movieId: UUID!,
$newTitle: String!)
@auth(level: USER)
@transaction {
# Step 1: Query and check
query @redact {
moviePermission( # Look up a join table called MoviePermission with a compound key.
key: {movieId: $movieId, userId_expr: "auth.uid"}
) {
# Check if the user has the editor role for the movie. `this` is the string value of `role`.
# If the parent moviePermission is null, the @check will also fail automatically.
role @check(expr: "this == 'editor'", message: "You must be an editor of this movie to update title")
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
Если возвращаемое поле встречается несколько раз, поскольку какой-либо предок является списком, каждое вхождение проверяется с помощью this к соответствующему значению.
Для любого заданного пути, если предок равен null или [] , то поле не будет достигнуто, и вычисление CEL для этого пути будет пропущено. Другими словами, вычисление происходит только тогда, когда this значение null или не равно null , но никогда не undefined .
Если само поле представляет собой список или объект, this имеет ту же структуру (включая всех потомков, выбранных в случае объектов), как показано в следующем примере.
mutation UpdateMovieTitle2($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query {
moviePermissions( # Now we query for a list of all matching MoviePermissions.
where: {movieId: {eq: $movieId}, userId: {eq_expr: "auth.uid"}}
# This time we execute the @check on the list, so `this` is the list of objects.
# We can use the `.exists` macro to check if there is at least one matching entry.
) @check(expr: "this.exists(p, p.role == 'editor')", message: "You must be an editor of this movie to update title") {
role
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
Сложный синтаксис выражений
Более сложные выражения можно создавать, комбинируя операторы && и || .
mutation UpsertUser($username: String!) @auth(expr: "(auth != null) && (vars.username == 'joe')")
В следующем разделе описаны все доступные операторы.
Операторы и приоритет операторов
Используйте следующую таблицу в качестве справочника по операторам и их соответствующему приоритету.
Пусть заданы произвольные выражения a и b , поле f и индекс i .
| Оператор | Описание | Ассоциативность |
|---|---|---|
a[i] a() af | Индекс, вызов, доступ к полю | слева направо |
!a -a | Унарное отрицание | справа налево |
a/ba%ba*b | операторы умножения | слева направо |
a+b ab | Аддитивные операторы | слева направо |
a>b a>=b a<b a<=b | Реляционные операторы | слева направо |
a in b | Наличие в списке или на карте | слева направо |
type(a) == t | Сравнение типов, где t может быть bool, int, float, number, string, list, map, timestamp или duration. | слева направо |
a==ba!=b | Операторы сравнения | слева направо |
a && b | Условное И | слева направо |
a || b | Условное ИЛИ | слева направо |
a ? true_value : false_value | Тройное выражение | слева направо |
Данные в токенах аутентификации
Объект auth.token может содержать следующие значения:
| Поле | Описание |
|---|---|
email | Адрес электронной почты, связанный с учетной записью, если таковой имеется. |
email_verified | true если пользователь подтвердил наличие доступа к адресу email . Некоторые провайдеры автоматически проверяют принадлежащие им адреса электронной почты. |
phone_number | Номер телефона, связанный с учетной записью, если таковой имеется. |
name | Отображаемое имя пользователя, если оно задано. |
sub | Идентификатор пользователя в Firebase (Firebase UID). Этот идентификатор уникален в рамках одного проекта. |
firebase.identities | Словарь всех идентификаторов, связанных с учетной записью этого пользователя. Ключами словаря могут быть любые из следующих: email , phone , google.com , facebook.com , github.com , twitter.com . Значения словаря представляют собой массивы уникальных идентификаторов для каждого поставщика идентификации, связанного с учетной записью. Например, auth.token.firebase.identities["google.com"][0] содержит первый идентификатор пользователя Google, связанный с учетной записью. |
firebase.sign_in_provider | Указан поставщик услуг авторизации, использованный для получения этого токена. Может принимать одно из следующих значений: custom , password , phone , anonymous , google.com , facebook.com , github.com , twitter.com . |
firebase.tenant | Идентификатор арендатора (tenantId), связанный с учетной записью, если он присутствует. Например, tenant2-m6tyz |
Дополнительные поля в токенах JWT ID
Вы также можете получить доступ к следующим полям auth.token :
| Пользовательские заявки на получение токенов | ||
|---|---|---|
alg | Алгоритм | "RS256" |
iss | Эмитент | Адрес электронной почты учетной записи службы вашего проекта |
sub | Предмет | Адрес электронной почты учетной записи службы вашего проекта |
aud | Аудитория | "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit" |
iat | Выпущено в момент | Текущее время в секундах с начала эпохи UNIX. |
exp | Срок действия | Время в секундах с начала эпохи UNIX, по истечении которого срок действия токена истекает. Максимальное время может быть на 3600 секунд позже , чем iat UNIX.Примечание: это управляет только временем истечения срока действия самого пользовательского токена . Но как только вы авторизуете пользователя с помощью signInWithCustomToken() , он останется авторизованным на устройстве до тех пор, пока его сессия не будет аннулирована или пользователь не выйдет из системы. |
<claims> (необязательно) | Дополнительные пользовательские утверждения, которые можно включить в токен и получить к ним доступ через auth.token (или request.auth.token ) в выражениях. Например, если вы создадите пользовательское утверждение adminClaim , вы сможете получить к нему доступ через auth.token.adminClaim . | |
Что дальше?
- Firebase SQL Connect предоставляет Admin SDK, позволяющий выполнять запросы и изменения данных из привилегированных сред .
- Подробнее о безопасности IAM можно узнать в руководстве по управлению службами и базами данных .
Firebase SQL Connect обеспечивает надежную безопасность на стороне клиента благодаря следующим функциям:
- Авторизация мобильных и веб-клиентов
- Индивидуальные средства управления авторизацией на уровне запросов и изменений
- Аттестация приложений с помощью Firebase App Check .
SQL Connect расширяет возможности этой системы безопасности за счет следующих функций:
- Авторизация на стороне сервера
- Безопасность пользователей Firebase и Cloud SQL с помощью IAM.
Авторизация запросов и изменений данных клиента.
SQL Connect полностью интегрирован с Firebase Authentication , поэтому вы можете использовать подробные данные о пользователях, получающих доступ к вашим данным (аутентификация), при проектировании системы, определяющей, к каким данным эти пользователи имеют доступ (авторизация).
SQL Connect предоставляет директиву @auth для запросов и мутаций, которая позволяет установить уровень аутентификации, необходимый для авторизации операции. В этом руководстве представлено описание директивы @auth с примерами .
Кроме того, SQL Connect поддерживает выполнение запросов, встроенных в мутации, поэтому вы можете получить дополнительные критерии авторизации, которые вы сохранили в своей базе данных, и использовать эти критерии в директивах @check , чтобы решить, разрешены ли содержащиеся в них мутации. В этом случае авторизации директива @redact позволяет управлять тем, возвращаются ли результаты запроса клиентам по протоколу передачи, а встроенный запрос опускается в сгенерированных SDK. Введение в эти директивы с примерами можно найти здесь.
Разберитесь в директиве @auth
Вы можете параметризовать директиву @auth , чтобы она соответствовала одному из нескольких предустановленных уровней доступа, охватывающих множество распространенных сценариев доступа. Эти уровни варьируются от PUBLIC (разрешает запросы и изменения данных от всех клиентов без какой-либо аутентификации) до NO_ACCESS (запрещает запросы и изменения данных за пределами привилегированных серверных сред с использованием Firebase Admin SDK ). Каждый из этих уровней связан с потоками аутентификации, предоставляемыми Firebase Authentication .
| Уровень | Определение |
|---|---|
PUBLIC | Операцию может выполнить любой пользователь, с аутентификацией или без неё. |
USER_ANON | Любой идентифицированный пользователь, включая тех, кто вошел в систему анонимно с помощью Firebase Authentication , имеет право выполнять запрос или мутацию. |
USER | Любой пользователь, вошедший в систему с использованием Firebase Authentication имеет право выполнять запрос или мутацию, за исключением анонимных пользователей. |
USER_EMAIL_VERIFIED | Любой пользователь, вошедший в систему с помощью Firebase Authentication и подтвердивший свой адрес электронной почты, имеет право выполнять запрос или мутацию. |
NO_ACCESS | Данная операция не может быть выполнена вне контекста Admin SDK. |
Используя эти предустановленные уровни доступа в качестве отправной точки, вы можете определить сложные и надежные проверки авторизации в директиве @auth , используя фильтры where и выражения Common Expression Language (CEL), оцениваемые на сервере.
Используйте директиву @auth для реализации распространенных сценариев авторизации.
Заданные уровни доступа являются отправной точкой для авторизации.
Уровень доступа USER — это наиболее полезный базовый уровень, с которого можно начать.
Полностью защищенный доступ будет основан на уровне USER , а также на фильтрах и выражениях, проверяющих атрибуты пользователя, атрибуты ресурсов, роли и другие параметры. Уровни USER_ANON и USER_EMAIL_VERIFIED являются вариантами уровня USER .
Синтаксис выражений позволяет оценивать данные с помощью объекта auth , представляющего данные аутентификации, передаваемые вместе с операциями, как стандартные данные в токенах аутентификации, так и пользовательские данные в токенах. Список полей, доступных в объекте auth , см. в справочном разделе .
Конечно, существуют сценарии, когда уровень доступа PUBLIC является правильным начальным уровнем. Однако, уровень доступа всегда является отправной точкой, и для обеспечения надежной безопасности необходимы дополнительные фильтры и выражения.
В этом руководстве теперь приведены примеры того, как создавать приложения на основе USER и PUBLIC .
Показательный пример
Приведенные ниже примеры передовой практики относятся к следующей схеме для платформы ведения блогов, где определенный контент доступен только по платному тарифному плану.
Подобная платформа, скорее всего, будет моделировать Users и Posts .
type User @table(key: "uid") {
uid: String!
name: String
birthday: Date
createdAt: Timestamp! @default(expr: "request.time")
}
type Post @table {
author: User!
text: String!
# "one of 'draft', 'public', or 'pro'"
visibility: String! @default(value: "draft")
# "the time at which the post should be considered published. defaults to
# immediately"
publishedAt: Timestamp! @default(expr: "request.time")
createdAt: Timestamp! @default(expr: "request.time")
updatedAt: Timestamp! @default(expr: "request.time")
}
Ресурсы, принадлежащие пользователям
Firebase рекомендует писать фильтры и выражения, проверяющие принадлежность ресурса пользователю, в следующих случаях — принадлежность объекта Posts .
В следующих примерах данные из токенов аутентификации считываются и сравниваются с помощью выражений. Типичный шаблон — использование выражений типа where: {authorUid: {eq_expr: "auth.uid"}} для сравнения сохраненного authorUid с auth.uid (идентификатором пользователя), переданным в токене аутентификации.
Создавать
Данная процедура авторизации начинается с добавления значения auth.uid из токена авторизации к каждой новой Post в качестве поля authorUid , что позволяет сравнивать данные в последующих проверках авторизации.
# Create a new post as the current user
mutation CreatePost($text: String!, $visibility: String) @auth(level: USER) {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Обновлять
Когда клиент пытается обновить Post , вы можете проверить переданный auth.uid на соответствие сохраненному authorUid .
# Update one of the current user's posts
mutation UpdatePost($id: UUID!, $text: String, $visibility: String) @auth(level:USER) {
post_update(
# only update posts whose author is the current user
first: { where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}
}}
data: {
text: $text
visibility: $visibility
# insert the current server time for updatedAt
updatedAt_expr: "request.time"
}
)
}
Удалить
Аналогичный метод используется для авторизации операций удаления.
# Delete one of the current user's posts
mutation DeletePost($id: UUID!) @auth(level: USER) {
post_delete(
# only delete posts whose author is the current user
first: { where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}
}}
)
}
# Common display information for a post
fragment DisplayPost on Post {
id, text, createdAt, updatedAt
author { uid, name }
}
Список
# List all posts belonging to the current user
query ListMyPosts @auth(level: USER) {
posts(where: {
userUid: {eq_expr: "auth.uid"}
}) {
# See the fragment above
...DisplayPost
# also show visibility since it is user-controlled
visibility
}
}
Получать
# Get a post only if it belongs to the current user
query GetMyPost($id: UUID!) @auth(level: USER) {
post(key: {id: $id},
first: {where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}}
}}, {
# See the fragment above
...DisplayPost
# also show visibility since it is user-controlled
visibility
}
}
Фильтрация данных
Система авторизации SQL Connect позволяет создавать сложные фильтры в сочетании с предустановленными уровнями доступа, такими как PUBLIC , а также с использованием данных из токенов авторизации.
Система авторизации также позволяет использовать только выражения, без указания базового уровня доступа, как показано в некоторых из следующих примеров.
Фильтрация по атрибутам ресурса
В данном случае авторизация не основана на токенах аутентификации, поскольку базовый уровень безопасности установлен на PUBLIC . Однако мы можем явно указать в нашей базе данных, что доступ к ней разрешен для всех пользователей; предположим, у нас есть записи Post в базе данных с уровнем visibility "public".
# List all posts marked as 'public' visibility
query ListPublicPosts @auth(level: PUBLIC) {
posts(where: {
# Test that visibility is "public"
visibility: {eq: "public"}
# Only display articles that are already published
publishedAt: {lt_expr: "request.time"}
}) {
# see the fragment above
...DisplayPost
}
}
Фильтрация по утверждениям пользователей
Предположим, вы настроили пользовательские утверждения, которые передают токены аутентификации для идентификации пользователей, использующих тарифный план «Pro» для вашего приложения, помеченный полем auth.token.plan в токене аутентификации. Ваши выражения могут проверять это поле.
# List all public or pro posts, only permitted if user has "pro" plan claim
query ProListPosts @auth(expr: "auth.token.plan == 'pro'") {
posts(where: {
# display both public posts and "pro" posts
visibility: {in: ['public', 'pro']},
# only display articles that are already published
publishedAt: {lt_expr: "request.time"},
}) {
# see the fragment above
...DisplayPost
# show visibility so pro users can see which posts are pro\
visibility
}
}
Фильтрация по порядку + лимиту
Или, например, вы могли установить visibility записей в Post таким образом, чтобы они соответствовали контенту, доступному для пользователей с платными правами, но для предварительного просмотра или ознакомительного списка данных можно дополнительно ограничить количество возвращаемых записей.
# Show 2 oldest Pro post as a preview
query ProTeaser @auth(level: USER) {
posts(
where: {
# show only pro posts
visibility: {eq: "pro"}
# that have already been published more than 30 days ago
publishedAt: {lt_time: {now: true, sub: {days: 30}}}
},
# order by publish time
orderBy: [{publishedAt: DESC}],
# only return two posts
limit: 2
) {
# See the fragment above
...DisplayPost
}
}
Фильтр по роли
Если в вашем пользовательском запросе определена роль admin , вы можете соответствующим образом тестировать и авторизовывать операции.
# List all posts unconditionally iff the current user has an admin claim
query AdminListPosts @auth(expr: "auth.token.admin == true") {
posts { ...DisplayPost }
}
Добавьте директивы @check и @redact для поиска данных авторизации.
Один из распространенных сценариев использования авторизации включает хранение пользовательских ролей авторизации в базе данных, например, в специальной таблице разрешений, и использование этих ролей для авторизации изменений, таких как создание, обновление или удаление данных.
Используя поиск данных авторизации, вы можете запрашивать роли на основе идентификатора пользователя и использовать выражения CEL для определения того, разрешена ли мутация. Например, вы можете написать мутацию UpdateMovieTitle , которая позволит авторизованному клиенту обновлять названия фильмов.
В дальнейшем обсуждении будем исходить из того, что база данных приложения для отзывов о фильмах хранит роль авторизации в таблице MoviePermission .
# MoviePermission
# Suppose a user has an authorization role with respect to records in the Movie table
type MoviePermission @table(key: ["movie", "user"]) {
movie: Movie! # implies another field: movieId: UUID!
user: User!
role: String!
}
Применение при мутациях
В приведенном ниже примере реализации мутация UpdateMovieTitle включает поле query для получения данных из MoviePermission , а также следующие директивы для обеспечения безопасности и надежности операции:
- Директива
@transaction, гарантирующая атомарное завершение или сбой всех запросов и проверок авторизации. - Директива
@redactисключает результаты запроса из ответа, что означает, что проверка авторизации выполняется на сервере SQL Connect но конфиденциальные данные не передаются клиенту. Пара директив
@checkдля оценки логики авторизации в результатах запроса, например, для проверки того, имеет ли данный userID соответствующую роль для внесения изменений.
mutation UpdateMovieTitle($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query @redact {
moviePermission( # Look up a join table called MoviePermission with a compound key.
key: {movieId: $movieId, userId_expr: "auth.uid"}
# Step 1a: Use @check to test if the user has any role associated with the movie
# Here the `this` binding refers the lookup result, i.e. a MoviePermission object or null
# The `this != null` expression could be omitted since rejecting on null is default behavior
) @check(expr: "this != null", message: "You do not have access to this movie") {
# Step 1b: Check if the user has the editor role for the movie
# Next we execute another @check; now `this` refers to the contents of the `role` field
role @check(expr: "this == 'editor'", message: "You must be an editor of this movie to update title")
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
Use in queries
Authorization data lookups are also useful to restrict queries based on roles or other restrictions.
In the following example, which also uses the MoviePermission schema, the query checks whether a requestor has an appropriate "admin" role to view users who can edit a movie.
query GetMovieEditors($movieId: UUID!) @auth(level: PUBLIC) {
moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
}
moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
user {
id
username
}
}
}
Antipatterns to avoid in authorization
The previous section covers patterns to follow when using the @auth directive.
You should also be aware of important antipatterns to avoid.
Avoid passing user attributes IDs and auth token parameters in query and mutation arguments
Firebase Authentication is a powerful tool for presenting authentication flows and securely capturing authentication data such as registered user IDs and numerous fields stored in auth tokens.
It's not a recommended practice to pass user IDs and auth token data in query and mutation arguments.
# Antipattern!
# This incorrectly allows any user to view any other user's posts
query AllMyPosts($userId: String!) @auth(level: USER) {
posts(where: {authorUid: {eq: $userId}}) {
id, text, createdAt
}
}
Avoid using the USER access level without any filters
As discussed several times in the guide, the core access levels like USER , USER_ANON , USER_EMAIL_VERIFIED are baselines and starting points for authorization checks, to be enhanced with filters and expressions. Using these levels without a corresponding filter or expression that checks which user is performing the request is essentially equivalent to using the PUBLIC level.
# Antipattern!
# This incorrectly allows any user to view all documents
query ListDocuments @auth(level: USER) {
documents {
id
title
text
}
}
Avoid using PUBLIC or USER access level for prototyping
To speed up development, it can be tempting to set all operations to the PUBLIC access level or to USER access level without further enhancements to authorize all operations and let you quickly test your code.
When you've done very initial prototyping this way, begin to switch from NO_ACCESS to production-ready authorization with PUBLIC and USER levels. However, don't deploy them as PUBLIC or USER without adding additional logic as shown in this guide.
# Antipattern!
# This incorrectly allows anyone to delete any post
mutation DeletePost($id: UUID!) @auth(level: PUBLIC) {
post: post_delete(
id: $id,
)
}
Avoid basing authorization on unverified email addresses
Granting access to users on a particular domain is a great way to limit access. However, anyone can claim to own an email during sign-in. Ensure you only grant access to email addresses that have been verified through Firebase Authentication.
# Antipattern!
# Anyone can claim an email address during sign-in
mutation CreatePost($text: String!, $visibility: String) @auth(expr: "auth.token.email.endsWith('@example.com')") {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Also check auth.token.email_verified
mutation CreatePost($text: String!, $visibility: String) @auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@example.com')") {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Audit authorization with the Firebase CLI
As indicated earlier, preset access levels such as PUBLIC and USER are the starting point for robust authorization, and should be used with additional filter and expression-based authorization checks. They shouldn't be used on their own without careful consideration of the use case.
SQL Connect helps you audit your authorization strategy by analyzing your connector code when you deploy to the server using firebase deploy from the Firebase CLI. You can use this audit to help you review your codebase.
When you deploy your connectors, the CLI will output assessments for existing, modified and new operation code in your connector.
For modified and new operations, the CLI issues warnings and prompts you for confirmation when you use certain access levels in your new operations, or when you modify existing operations to use those access levels.
Warnings and prompts always occur for:
-
PUBLIC
And, warnings and prompts occur on the following access levels when you don't augment them with filters using auth.uid :
-
USER -
USER_ANON -
USER_EMAIL_VERIFIED
Suppress insecure operation warnings with the @auth(insecureReason:) argument
In many cases, you will conclude that using the PUBLIC and USER* access levels is perfectly appropriate.
When your connector contains many operations, you may want clearer, more relevant security audit output that omits operations that would normally trigger a warning, but you know have the correct access level.
You can suppress warnings for such operations with @auth(insecureReason:) . For example:
query listItem @auth(level: PUBLIC, insecureReason: "This operation is safe to expose to the public.")
{
items {
id name
}
}
Use Firebase App Check for app attestation
Authentication and authorization are critical components of SQL Connect security. Authentication and authorization combined with app attestation makes for a very robust security solution.
With attestation through Firebase App Check , devices running your app will use an app or device attestation provider that attests that SQL Connect operations originate from your authentic app and requests originate from an authentic, untampered device. This attestation is attached to every request your app makes to SQL Connect .
To learn how to enable App Check for SQL Connect and include its client SDK in your app, have a look at the App Check overview .
Authentication levels for the @auth(level) directive
The following table lists all standard access levels and their CEL equivalents. Authentication levels are listed from broad to narrow -- each level encompasses all users who match following levels.
| Уровень | Определение |
|---|---|
PUBLIC | The operation can be executed by anyone with or without authentication. Considerations: Data can be read or modified by any user. Firebase recommends this level of authorization for publicly-browsable data like product or media listings. See the best practice examples and alternatives . Equivalent to @auth(expr: "true")@auth filters and expressions cannot be used in combination with this access level. Any such expressions will fail with a 400 bad request error. |
USER_ANON | Any identified user, including those who have logged in anonymously with Firebase Authentication , is authorized to perform the query or mutation. Note: USER_ANON is a superset of USER .Considerations: Note that you must carefully design your queries and mutations for this level of authorization. This level allows user to be logged in anonymously (automatic sign-in tied only to a user device) with Authentication , and does not on its own perform other checks on, for example, whether data belongs to the user. See the best practice examples and alternatives . Since Authentication anonymous login flows issue a uid , the USER_ANON level is equivalent to@auth(expr: "auth.uid != nil") |
USER | Any user who has logged in with Firebase Authentication is authorized to perform the query or mutation except anonymous sign-in users. Considerations: Note that you must carefully design your queries and mutations for this level of authorization. This level only checks that the user is logged in with Authentication , and does not on its own perform other checks on, for example, whether data belongs to the user. See the best practice examples and alternatives . Equivalent to @auth(expr: "auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'")" |
USER_EMAIL_VERIFIED | Any user who has logged in with Firebase Authentication with a verified email address is authorized to perform the query or mutation. Considerations: Since email verification is performed using Authentication , it's based on a more robust Authentication method, thus this level provides additional security compared to USER or USER_ANON . This level only checks that the user is logged in with Authentication with a verified email, and does not on its own perform other checks on, for example, whether data belongs to the user. See the best practice examples and alternatives .Equivalent to @auth(expr: "auth.uid != nil && auth.token.email_verified")" |
NO_ACCESS | This operation cannot be executed outside an Admin SDK context. Equivalent to @auth(expr: "false") |
CEL Reference for @auth(expr)
As shown in examples elsewhere in this guide, you can and should use expressions defined in Common Expression Language (CEL) to control authorization for SQL Connect using @auth(expr:) and @check directives.
This section covers CEL syntax relevant to creating expressions for these directives.
Complete reference information for CEL is provided in the CEL specification .
Test variables passed in queries and mutations
@auth(expr) syntax allows you access and test variables from queries and mutations.
For example, you can include an operation variable, such as $status , using vars.status .
mutation Update($id: UUID!, $status: Any) @auth(expr: "has(vars.status)")
Data available to expressions: request, response, this
You use data for:
- Evaluation with CEL expressions in
@auth(expr:)and@check(expr:)directives - Assignment using server expressions,
<field>_expr.
The @auth(expr:) , @check(expr:) , @refresh(onMutationExecuted:) CEL expressions can evaluate the following:
-
request.operationName -
vars(alias forrequest.variables) -
auth(alias forrequest.auth)
In mutations, you can access and assign the contents of:
-
response(to check partial results in multi-step logic)
Additionally, @check(expr:) expressions can evaluate:
-
this(the value of the current field) -
response(to check partial results in multi-step logic)
And @refresh(onMutationExecuted:) expressions have the following binding:
-
mutation.variables -
mutation.auth
The request.operationName binding
The request.operarationName binding stores the type of operation, either query or mutation.
The vars binding (request.vars)
The vars binding allows your expressions to access all variables passed in your query or mutation.
You can use vars.<variablename> in an expression as an alias for the fully-qualified request.variables.<variablename> :
# The following are equivalent
mutation StringType($v: String!) @auth(expr: "vars.v == 'hello'")
mutation StringType($v: String!) @auth(expr: "request.variables.v == 'hello'")
The auth binding (request.auth)
Authentication identifies users requesting access to your data and provides that information as a binding you can build on in your expressions.
In your filters and expressions, you can use auth as an alias for request.auth .
The auth binding contains the following information:
-
uid: A unique user ID, assigned to the requesting user. -
token: A map of values collected by Authentication .
For more details on the contents of auth.token see Data in auth tokens
The response binding
The response binding contains the data being assembled by the server in response to a query or mutation as that data is being assembled .
As the operation proceeds, as each step is completed successfully, response contains response data from successfully-completed steps.
The response binding is structured according the shape of its associated operation, including (multiple) nested fields and (if applicable) embedded queries.
Note that when you access embedded query response data , fields can contain any data type, depending on the data requested in the embedded query; when you access data returned by mutation fields like _insert s and _delete s, they may contain UUID keys, number of deletes, nulls (see the mutations reference ).
Например:
- In a mutation that contains an embedded query, the
responsebinding contains lookup data atresponse.query.<fieldName>.<fieldName>...., in this case,response.query.todoListandresponse.query.todoList.priority.
mutation CheckTodoPriority(
$uniqueListName: String!
) {
# This query is identified as `response.query`
query @check(expr: "response.query.todoList.priority == 'high'", message: "This list is not for high priority items!") {
# This field is identified as `response.query.todoList`
todoList(where: { name: $uniqueListName }) {
# This field is identified as `response.query.todoList.priority`
priority
}
}
}
- In a multi-step mutation, for example with multiple
_insertfields, theresponsebinding contains partial data atresponse.<fieldName>.<fieldName>...., in this case,response.todoList_insert.id.
mutation CreateTodoListWithFirstItem(
$listName: String!,
$itemContent: String!
) @transaction {
# Step 1
todoList_insert(data: {
id_expr: "uuidV4()",
name: $listName,
})
# Step 2:
todo_insert(data: {
listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
content: $itemContent,
})
}
The this binding
The binding this evaluates to the field that the @check directive is attached to. In a basic case, you might evaluate single-valued query results.
mutation UpdateMovieTitle (
$movieId: UUID!,
$newTitle: String!)
@auth(level: USER)
@transaction {
# Step 1: Query and check
query @redact {
moviePermission( # Look up a join table called MoviePermission with a compound key.
key: {movieId: $movieId, userId_expr: "auth.uid"}
) {
# Check if the user has the editor role for the movie. `this` is the string value of `role`.
# If the parent moviePermission is null, the @check will also fail automatically.
role @check(expr: "this == 'editor'", message: "You must be an editor of this movie to update title")
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
If the returned field occurs multiple times because any ancestor is a list, each occurrence is tested with this bound to each value.
For any given path, if an ancestor is null or [] , the field won't be reached and the CEL evaluation will be skipped for that path. In other words, evaluation only takes place when this is null or non- null , but never undefined .
When the field itself is a list or object, this follows the same structure (including all descendants selected in case of objects), as illustrated in the following example.
mutation UpdateMovieTitle2($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query {
moviePermissions( # Now we query for a list of all matching MoviePermissions.
where: {movieId: {eq: $movieId}, userId: {eq_expr: "auth.uid"}}
# This time we execute the @check on the list, so `this` is the list of objects.
# We can use the `.exists` macro to check if there is at least one matching entry.
) @check(expr: "this.exists(p, p.role == 'editor')", message: "You must be an editor of this movie to update title") {
role
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
Complex expression syntax
You can write more complex expressions by combining with the && and || operators.
mutation UpsertUser($username: String!) @auth(expr: "(auth != null) && (vars.username == 'joe')")
The following section describes all available operators.
Operators and operator precedence
Use the following table as a reference for operators and their corresponding precedence.
Given arbitrary expressions a and b , a field f , and an index i .
| Оператор | Описание | Associativity |
|---|---|---|
a[i] a() af | Index, call, field access | left to right |
!a -a | Unary negation | справа налево |
a/ba%ba*b | операторы умножения | left to right |
a+b ab | Additive operators | left to right |
a>b a>=b a<b a<=b | Relational operators | left to right |
a in b | Existence in list or map | left to right |
type(a) == t | Type comparison, where t can be bool, int, float, number, string, list, map, timestamp, or duration | left to right |
a==ba!=b | Операторы сравнения | left to right |
a && b | Conditional AND | left to right |
a || b | Conditional OR | left to right |
a ? true_value : false_value | Ternary expression | left to right |
Data in auth tokens
The auth.token object may contain the following values:
| Поле | Описание |
|---|---|
email | The email address associated with the account, if present. |
email_verified | true if the user has verified they have access to the email address. Some providers automatically verify email addresses they own. |
phone_number | The phone number associated with the account, if present. |
name | The user's display name, if set. |
sub | The user's Firebase UID. This is unique within a project. |
firebase.identities | Dictionary of all the identities that are associated with this user's account. The keys of the dictionary can be any of the following: email , phone , google.com , facebook.com , github.com , twitter.com . The values of the dictionary are arrays of unique identifiers for each identity provider associated with the account. For example, auth.token.firebase.identities["google.com"][0] contains the first Google user ID associated with the account. |
firebase.sign_in_provider | The sign-in provider used to obtain this token. Can be one of the following strings: custom , password , phone , anonymous , google.com , facebook.com , github.com , twitter.com . |
firebase.tenant | The tenantId associated with the account, if present. For example, tenant2-m6tyz |
Additional fields in JWT ID tokens
You can also access the following auth.token fields:
| Custom Token Claims | ||
|---|---|---|
alg | Алгоритм | "RS256" |
iss | Эмитент | Your project's service account email address |
sub | Предмет | Your project's service account email address |
aud | Аудитория | "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit" |
iat | Issued-at time | The current time, in seconds since the UNIX epoch |
exp | Срок действия | The time, in seconds since the UNIX epoch, at which the token expires. It can be a maximum of 3600 seconds later than the iat .Note: this only controls the time when the custom token itself expires. But once you sign a user in using signInWithCustomToken() , they will remain signed in into the device until their session is invalidated or the user signs out. |
<claims> (optional) | Optional custom claims to include in token, which can be accessed through auth.token (or request.auth.token ) in expressions. For example, if you create a custom claim adminClaim , you can access it with auth.token.adminClaim . | |
Что дальше?
- Firebase SQL Connect provides an Admin SDK to let you perform queries and mutations from privileged environments .
- Learn about IAM security in the guide for managing services and databases .
Firebase SQL Connect provides robust client-side security with:
- Mobile and web client authorization
- Individual query- and mutation-level authorization controls
- App attestation with Firebase App Check .
SQL Connect extends this security with:
- Server-side authorization
- Firebase project and Cloud SQL user security with IAM.
Authorize client queries and mutations
SQL Connect is fully integrated with Firebase Authentication , so you can use rich data about users who are accessing your data (authentication) in your design for what data those users can access (authorization).
SQL Connect provides an @auth directive for queries and mutations that lets you set the level of authentication required to authorize the operation. This guide introduces the @auth directive, with examples .
In addition, SQL Connect supports execution of queries embedded in mutations, so you can retrieve additional authorization criteria you've stored in your database, and use those criteria in @check directives to decide if enclosing mutations are authorized. For this authorization case, the @redact directive allows you control whether query results are returned to clients in the wire protocol and the embedded query omitted in generated SDKs. Find the introduction to these directives, with examples .
Understand the @auth directive
You can parameterize the @auth directive to follow one of several preset access levels that cover many common access scenarios. These levels range from PUBLIC (which allows queries and mutations from all clients without authentication of any kind) to NO_ACCESS (which disallows queries and mutations outside of privileged server environments using the Firebase Admin SDK ). Each of these levels is correlated with authentication flows provided by Firebase Authentication .
| Уровень | Определение |
|---|---|
PUBLIC | The operation can be executed by anyone with or without authentication. |
USER_ANON | Any identified user, including those who have logged in anonymously with Firebase Authentication , is authorized to perform the query or mutation. |
USER | Any user who has logged in with Firebase Authentication is authorized to perform the query or mutation except anonymous sign-in users. |
USER_EMAIL_VERIFIED | Any user who has logged in with Firebase Authentication with a verified email address is authorized to perform the query or mutation. |
NO_ACCESS | This operation cannot be executed outside an Admin SDK context. |
Using these preset access levels as a starting point, you can define complex and robust authorization checks in the @auth directive using where filters and Common Expression Language (CEL) expressions evaluated on the server.
Use the @auth directive to implement common authorization scenarios
The preset access levels are the starting point for authorization.
The USER access level is the most widely useful basic level to start with.
Fully secure access will build on the USER level plus filters and expressions that check user attributes, resource attributes, roles and other checks. The USER_ANON and USER_EMAIL_VERIFIED levels are variations on the USER case.
Expression syntax lets you evaluate data using an auth object representing authentication data passed with operations, both standard data in auth tokens and custom data in tokens. For the list of fields available in the auth object, see the reference section .
There are of course use cases where PUBLIC is the correct access level to start with. Again, an access level is always a starting point, and additional filters and expressions are needed for robust security.
This guide now gives examples of how to build on USER and PUBLIC .
A motivating example
The following best practice examples refer to the following schema for a blogging platform with certain content locked behind a payment plan.
Such a platform would likely model Users and Posts .
type User @table(key: "uid") {
uid: String!
name: String
birthday: Date
createdAt: Timestamp! @default(expr: "request.time")
}
type Post @table {
author: User!
text: String!
# "one of 'draft', 'public', or 'pro'"
visibility: String! @default(value: "draft")
# "the time at which the post should be considered published. defaults to
# immediately"
publishedAt: Timestamp! @default(expr: "request.time")
createdAt: Timestamp! @default(expr: "request.time")
updatedAt: Timestamp! @default(expr: "request.time")
}
User-owned resources
Firebase recommends that you write filters and expressions that test user ownership of a resource, in the following cases, ownership of Posts .
In the following examples, data from auth tokens is read and compared using expressions. The typical pattern is to use expressions like where: {authorUid: {eq_expr: "auth.uid"}} to compare a stored authorUid to the auth.uid (user ID) passed in the authentication token.
Создавать
This authorization practice starts by adding the auth.uid from the auth token to each new Post as an authorUid field to allow comparison in subsequence authorization tests.
# Create a new post as the current user
mutation CreatePost($text: String!, $visibility: String) @auth(level: USER) {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Обновлять
When a client attempts to update a Post , you can test the passed auth.uid against the stored authorUid .
# Update one of the current user's posts
mutation UpdatePost($id: UUID!, $text: String, $visibility: String) @auth(level:USER) {
post_update(
# only update posts whose author is the current user
first: { where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}
}}
data: {
text: $text
visibility: $visibility
# insert the current server time for updatedAt
updatedAt_expr: "request.time"
}
)
}
Удалить
The same technique is used to authorize delete operations.
# Delete one of the current user's posts
mutation DeletePost($id: UUID!) @auth(level: USER) {
post_delete(
# only delete posts whose author is the current user
first: { where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}
}}
)
}
# Common display information for a post
fragment DisplayPost on Post {
id, text, createdAt, updatedAt
author { uid, name }
}
Список
# List all posts belonging to the current user
query ListMyPosts @auth(level: USER) {
posts(where: {
userUid: {eq_expr: "auth.uid"}
}) {
# See the fragment above
...DisplayPost
# also show visibility since it is user-controlled
visibility
}
}
Получать
# Get a post only if it belongs to the current user
query GetMyPost($id: UUID!) @auth(level: USER) {
post(key: {id: $id},
first: {where: {
id: {eq: $id}
authorUid: {eq_expr: "auth.uid"}}
}}, {
# See the fragment above
...DisplayPost
# also show visibility since it is user-controlled
visibility
}
}
Filter Data
SQL Connect 's authorization system lets you write sophisticated filters combined with preset access levels like PUBLIC as well as by using data from auth tokens.
The authorization system also lets you use expressions only, without a base access level, as shown in some of the following examples.
Filter by resource attributes
Here, authorization is not based on auth tokens since the base security level is set to PUBLIC . But, we can explicitly set records in our database as suitable for public access; assume we have Post records in our database with visibility set to "public".
# List all posts marked as 'public' visibility
query ListPublicPosts @auth(level: PUBLIC) {
posts(where: {
# Test that visibility is "public"
visibility: {eq: "public"}
# Only display articles that are already published
publishedAt: {lt_expr: "request.time"}
}) {
# see the fragment above
...DisplayPost
}
}
Filter by user claims
Here, assume you've set up custom user claims that pass in auth tokens to identify users in a "pro" plan for your app, flagged with an auth.token.plan field in the auth token. Your expressions can test against this field.
# List all public or pro posts, only permitted if user has "pro" plan claim
query ProListPosts @auth(expr: "auth.token.plan == 'pro'") {
posts(where: {
# display both public posts and "pro" posts
visibility: {in: ['public', 'pro']},
# only display articles that are already published
publishedAt: {lt_expr: "request.time"},
}) {
# see the fragment above
...DisplayPost
# show visibility so pro users can see which posts are pro\
visibility
}
}
Filter by order + limit
Or again, you may have set visibility in Post records to identify they are content available for "pro" users, but for a preview or teaser listing of data, further limit the number of records returned.
# Show 2 oldest Pro post as a preview
query ProTeaser @auth(level: USER) {
posts(
where: {
# show only pro posts
visibility: {eq: "pro"}
# that have already been published more than 30 days ago
publishedAt: {lt_time: {now: true, sub: {days: 30}}}
},
# order by publish time
orderBy: [{publishedAt: DESC}],
# only return two posts
limit: 2
) {
# See the fragment above
...DisplayPost
}
}
Filter by role
If your custom claim defines an admin role, you can test and authorize operations accordingly.
# List all posts unconditionally iff the current user has an admin claim
query AdminListPosts @auth(expr: "auth.token.admin == true") {
posts { ...DisplayPost }
}
Add the @check and @redact directives to look up authorization data
A common authorization use case involves storing custom authorization roles in your database, for example in a special permissions table, and using those roles to authorize mutations to create, update, or delete data.
Using authorization data lookups, you can query for roles based on a userID and use CEL expressions to decide if the mutation is authorized. For example, you might want to write an UpdateMovieTitle mutation that lets an authorized client update movie titles.
For the rest of this discussion, assume the movie review app database stores an authorization role in a MoviePermission table.
# MoviePermission
# Suppose a user has an authorization role with respect to records in the Movie table
type MoviePermission @table(key: ["movie", "user"]) {
movie: Movie! # implies another field: movieId: UUID!
user: User!
role: String!
}
Use in mutations
In the following example implementation, the UpdateMovieTitle mutation includes a query field to retrieve data from MoviePermission , and the following directives to ensure the operation is secure and robust:
- A
@transactiondirective to ensure all authorization queries and checks are completed or fail atomically. - The
@redactdirective to omit query results from the response, meaning our authorization check is performed on the SQL Connect server but sensitive data is not exposed to the client. A pair of
@checkdirectives to evaluate authorization logic on query results, such as testing that a given userID has an appropriate role to make modifications.
mutation UpdateMovieTitle($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query @redact {
moviePermission( # Look up a join table called MoviePermission with a compound key.
key: {movieId: $movieId, userId_expr: "auth.uid"}
# Step 1a: Use @check to test if the user has any role associated with the movie
# Here the `this` binding refers the lookup result, i.e. a MoviePermission object or null
# The `this != null` expression could be omitted since rejecting on null is default behavior
) @check(expr: "this != null", message: "You do not have access to this movie") {
# Step 1b: Check if the user has the editor role for the movie
# Next we execute another @check; now `this` refers to the contents of the `role` field
role @check(expr: "this == 'editor'", message: "You must be an editor of this movie to update title")
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
Use in queries
Authorization data lookups are also useful to restrict queries based on roles or other restrictions.
In the following example, which also uses the MoviePermission schema, the query checks whether a requestor has an appropriate "admin" role to view users who can edit a movie.
query GetMovieEditors($movieId: UUID!) @auth(level: PUBLIC) {
moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
}
moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
user {
id
username
}
}
}
Antipatterns to avoid in authorization
The previous section covers patterns to follow when using the @auth directive.
You should also be aware of important antipatterns to avoid.
Avoid passing user attributes IDs and auth token parameters in query and mutation arguments
Firebase Authentication is a powerful tool for presenting authentication flows and securely capturing authentication data such as registered user IDs and numerous fields stored in auth tokens.
It's not a recommended practice to pass user IDs and auth token data in query and mutation arguments.
# Antipattern!
# This incorrectly allows any user to view any other user's posts
query AllMyPosts($userId: String!) @auth(level: USER) {
posts(where: {authorUid: {eq: $userId}}) {
id, text, createdAt
}
}
Avoid using the USER access level without any filters
As discussed several times in the guide, the core access levels like USER , USER_ANON , USER_EMAIL_VERIFIED are baselines and starting points for authorization checks, to be enhanced with filters and expressions. Using these levels without a corresponding filter or expression that checks which user is performing the request is essentially equivalent to using the PUBLIC level.
# Antipattern!
# This incorrectly allows any user to view all documents
query ListDocuments @auth(level: USER) {
documents {
id
title
text
}
}
Avoid using PUBLIC or USER access level for prototyping
To speed up development, it can be tempting to set all operations to the PUBLIC access level or to USER access level without further enhancements to authorize all operations and let you quickly test your code.
When you've done very initial prototyping this way, begin to switch from NO_ACCESS to production-ready authorization with PUBLIC and USER levels. However, don't deploy them as PUBLIC or USER without adding additional logic as shown in this guide.
# Antipattern!
# This incorrectly allows anyone to delete any post
mutation DeletePost($id: UUID!) @auth(level: PUBLIC) {
post: post_delete(
id: $id,
)
}
Avoid basing authorization on unverified email addresses
Granting access to users on a particular domain is a great way to limit access. However, anyone can claim to own an email during sign-in. Ensure you only grant access to email addresses that have been verified through Firebase Authentication.
# Antipattern!
# Anyone can claim an email address during sign-in
mutation CreatePost($text: String!, $visibility: String) @auth(expr: "auth.token.email.endsWith('@example.com')") {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Also check auth.token.email_verified
mutation CreatePost($text: String!, $visibility: String) @auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@example.com')") {
post_insert(data: {
# set the author's uid to the current user uid
authorUid_expr: "auth.uid"
text: $text
visibility: $visibility
})
}
Audit authorization with the Firebase CLI
As indicated earlier, preset access levels such as PUBLIC and USER are the starting point for robust authorization, and should be used with additional filter and expression-based authorization checks. They shouldn't be used on their own without careful consideration of the use case.
SQL Connect helps you audit your authorization strategy by analyzing your connector code when you deploy to the server using firebase deploy from the Firebase CLI. You can use this audit to help you review your codebase.
When you deploy your connectors, the CLI will output assessments for existing, modified and new operation code in your connector.
For modified and new operations, the CLI issues warnings and prompts you for confirmation when you use certain access levels in your new operations, or when you modify existing operations to use those access levels.
Warnings and prompts always occur for:
-
PUBLIC
And, warnings and prompts occur on the following access levels when you don't augment them with filters using auth.uid :
-
USER -
USER_ANON -
USER_EMAIL_VERIFIED
Suppress insecure operation warnings with the @auth(insecureReason:) argument
In many cases, you will conclude that using the PUBLIC and USER* access levels is perfectly appropriate.
When your connector contains many operations, you may want clearer, more relevant security audit output that omits operations that would normally trigger a warning, but you know have the correct access level.
You can suppress warnings for such operations with @auth(insecureReason:) . For example:
query listItem @auth(level: PUBLIC, insecureReason: "This operation is safe to expose to the public.")
{
items {
id name
}
}
Use Firebase App Check for app attestation
Authentication and authorization are critical components of SQL Connect security. Authentication and authorization combined with app attestation makes for a very robust security solution.
With attestation through Firebase App Check , devices running your app will use an app or device attestation provider that attests that SQL Connect operations originate from your authentic app and requests originate from an authentic, untampered device. This attestation is attached to every request your app makes to SQL Connect .
To learn how to enable App Check for SQL Connect and include its client SDK in your app, have a look at the App Check overview .
Authentication levels for the @auth(level) directive
The following table lists all standard access levels and their CEL equivalents. Authentication levels are listed from broad to narrow -- each level encompasses all users who match following levels.
| Уровень | Определение |
|---|---|
PUBLIC | The operation can be executed by anyone with or without authentication. Considerations: Data can be read or modified by any user. Firebase recommends this level of authorization for publicly-browsable data like product or media listings. See the best practice examples and alternatives . Equivalent to @auth(expr: "true")@auth filters and expressions cannot be used in combination with this access level. Any such expressions will fail with a 400 bad request error. |
USER_ANON | Any identified user, including those who have logged in anonymously with Firebase Authentication , is authorized to perform the query or mutation. Note: USER_ANON is a superset of USER .Considerations: Note that you must carefully design your queries and mutations for this level of authorization. This level allows user to be logged in anonymously (automatic sign-in tied only to a user device) with Authentication , and does not on its own perform other checks on, for example, whether data belongs to the user. See the best practice examples and alternatives . Since Authentication anonymous login flows issue a uid , the USER_ANON level is equivalent to@auth(expr: "auth.uid != nil") |
USER | Any user who has logged in with Firebase Authentication is authorized to perform the query or mutation except anonymous sign-in users. Considerations: Note that you must carefully design your queries and mutations for this level of authorization. This level only checks that the user is logged in with Authentication , and does not on its own perform other checks on, for example, whether data belongs to the user. See the best practice examples and alternatives . Equivalent to @auth(expr: "auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous'")" |
USER_EMAIL_VERIFIED | Any user who has logged in with Firebase Authentication with a verified email address is authorized to perform the query or mutation. Considerations: Since email verification is performed using Authentication , it's based on a more robust Authentication method, thus this level provides additional security compared to USER or USER_ANON . This level only checks that the user is logged in with Authentication with a verified email, and does not on its own perform other checks on, for example, whether data belongs to the user. See the best practice examples and alternatives .Equivalent to @auth(expr: "auth.uid != nil && auth.token.email_verified")" |
NO_ACCESS | This operation cannot be executed outside an Admin SDK context. Equivalent to @auth(expr: "false") |
CEL Reference for @auth(expr)
As shown in examples elsewhere in this guide, you can and should use expressions defined in Common Expression Language (CEL) to control authorization for SQL Connect using @auth(expr:) and @check directives.
This section covers CEL syntax relevant to creating expressions for these directives.
Complete reference information for CEL is provided in the CEL specification .
Test variables passed in queries and mutations
@auth(expr) syntax allows you access and test variables from queries and mutations.
For example, you can include an operation variable, such as $status , using vars.status .
mutation Update($id: UUID!, $status: Any) @auth(expr: "has(vars.status)")
Data available to expressions: request, response, this
You use data for:
- Evaluation with CEL expressions in
@auth(expr:)and@check(expr:)directives - Assignment using server expressions,
<field>_expr.
The @auth(expr:) , @check(expr:) , @refresh(onMutationExecuted:) CEL expressions can evaluate the following:
-
request.operationName -
vars(alias forrequest.variables) -
auth(alias forrequest.auth)
In mutations, you can access and assign the contents of:
-
response(to check partial results in multi-step logic)
Additionally, @check(expr:) expressions can evaluate:
-
this(the value of the current field) -
response(to check partial results in multi-step logic)
And @refresh(onMutationExecuted:) expressions have the following binding:
-
mutation.variables -
mutation.auth
The request.operationName binding
The request.operarationName binding stores the type of operation, either query or mutation.
The vars binding (request.vars)
The vars binding allows your expressions to access all variables passed in your query or mutation.
You can use vars.<variablename> in an expression as an alias for the fully-qualified request.variables.<variablename> :
# The following are equivalent
mutation StringType($v: String!) @auth(expr: "vars.v == 'hello'")
mutation StringType($v: String!) @auth(expr: "request.variables.v == 'hello'")
The auth binding (request.auth)
Authentication identifies users requesting access to your data and provides that information as a binding you can build on in your expressions.
In your filters and expressions, you can use auth as an alias for request.auth .
The auth binding contains the following information:
-
uid: A unique user ID, assigned to the requesting user. -
token: A map of values collected by Authentication .
For more details on the contents of auth.token see Data in auth tokens
The response binding
The response binding contains the data being assembled by the server in response to a query or mutation as that data is being assembled .
As the operation proceeds, as each step is completed successfully, response contains response data from successfully-completed steps.
The response binding is structured according the shape of its associated operation, including (multiple) nested fields and (if applicable) embedded queries.
Note that when you access embedded query response data , fields can contain any data type, depending on the data requested in the embedded query; when you access data returned by mutation fields like _insert s and _delete s, they may contain UUID keys, number of deletes, nulls (see the mutations reference ).
Например:
- In a mutation that contains an embedded query, the
responsebinding contains lookup data atresponse.query.<fieldName>.<fieldName>...., in this case,response.query.todoListandresponse.query.todoList.priority.
mutation CheckTodoPriority(
$uniqueListName: String!
) {
# This query is identified as `response.query`
query @check(expr: "response.query.todoList.priority == 'high'", message: "This list is not for high priority items!") {
# This field is identified as `response.query.todoList`
todoList(where: { name: $uniqueListName }) {
# This field is identified as `response.query.todoList.priority`
priority
}
}
}
- In a multi-step mutation, for example with multiple
_insertfields, theresponsebinding contains partial data atresponse.<fieldName>.<fieldName>...., in this case,response.todoList_insert.id.
mutation CreateTodoListWithFirstItem(
$listName: String!,
$itemContent: String!
) @transaction {
# Step 1
todoList_insert(data: {
id_expr: "uuidV4()",
name: $listName,
})
# Step 2:
todo_insert(data: {
listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
content: $itemContent,
})
}
The this binding
The binding this evaluates to the field that the @check directive is attached to. In a basic case, you might evaluate single-valued query results.
mutation UpdateMovieTitle (
$movieId: UUID!,
$newTitle: String!)
@auth(level: USER)
@transaction {
# Step 1: Query and check
query @redact {
moviePermission( # Look up a join table called MoviePermission with a compound key.
key: {movieId: $movieId, userId_expr: "auth.uid"}
) {
# Check if the user has the editor role for the movie. `this` is the string value of `role`.
# If the parent moviePermission is null, the @check will also fail automatically.
role @check(expr: "this == 'editor'", message: "You must be an editor of this movie to update title")
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
If the returned field occurs multiple times because any ancestor is a list, each occurrence is tested with this bound to each value.
For any given path, if an ancestor is null or [] , the field won't be reached and the CEL evaluation will be skipped for that path. In other words, evaluation only takes place when this is null or non- null , but never undefined .
When the field itself is a list or object, this follows the same structure (including all descendants selected in case of objects), as illustrated in the following example.
mutation UpdateMovieTitle2($movieId: UUID!, $newTitle: String!) @auth(level: USER) @transaction {
# Step 1: Query and check
query {
moviePermissions( # Now we query for a list of all matching MoviePermissions.
where: {movieId: {eq: $movieId}, userId: {eq_expr: "auth.uid"}}
# This time we execute the @check on the list, so `this` is the list of objects.
# We can use the `.exists` macro to check if there is at least one matching entry.
) @check(expr: "this.exists(p, p.role == 'editor')", message: "You must be an editor of this movie to update title") {
role
}
}
# Step 2: Act
movie_update(id: $movieId, data: {
title: $newTitle
})
}
Complex expression syntax
You can write more complex expressions by combining with the && and || operators.
mutation UpsertUser($username: String!) @auth(expr: "(auth != null) && (vars.username == 'joe')")
The following section describes all available operators.
Operators and operator precedence
Use the following table as a reference for operators and their corresponding precedence.
Given arbitrary expressions a and b , a field f , and an index i .
| Оператор | Описание | Associativity |
|---|---|---|
a[i] a() af | Index, call, field access | left to right |
!a -a | Unary negation | справа налево |
a/ba%ba*b | операторы умножения | left to right |
a+b ab | Additive operators | left to right |
a>b a>=b a<b a<=b | Relational operators | left to right |
a in b | Existence in list or map | left to right |
type(a) == t | Type comparison, where t can be bool, int, float, number, string, list, map, timestamp, or duration | left to right |
a==ba!=b | Операторы сравнения | left to right |
a && b | Conditional AND | left to right |
a || b | Conditional OR | left to right |
a ? true_value : false_value | Ternary expression | left to right |
Data in auth tokens
The auth.token object may contain the following values:
| Поле | Описание |
|---|---|
email | The email address associated with the account, if present. |
email_verified | true if the user has verified they have access to the email address. Some providers automatically verify email addresses they own. |
phone_number | The phone number associated with the account, if present. |
name | The user's display name, if set. |
sub | The user's Firebase UID. This is unique within a project. |
firebase.identities | Dictionary of all the identities that are associated with this user's account. The keys of the dictionary can be any of the following: email , phone , google.com , facebook.com , github.com , twitter.com . The values of the dictionary are arrays of unique identifiers for each identity provider associated with the account. For example, auth.token.firebase.identities["google.com"][0] contains the first Google user ID associated with the account. |
firebase.sign_in_provider | The sign-in provider used to obtain this token. Can be one of the following strings: custom , password , phone , anonymous , google.com , facebook.com , github.com , twitter.com . |
firebase.tenant | The tenantId associated with the account, if present. For example, tenant2-m6tyz |
Additional fields in JWT ID tokens
You can also access the following auth.token fields:
| Custom Token Claims | ||
|---|---|---|
alg | Алгоритм | "RS256" |
iss | Эмитент | Your project's service account email address |
sub | Предмет | Your project's service account email address |
aud | Аудитория | "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit" |
iat | Issued-at time | The current time, in seconds since the UNIX epoch |
exp | Срок действия | The time, in seconds since the UNIX epoch, at which the token expires. It can be a maximum of 3600 seconds later than the iat .Note: this only controls the time when the custom token itself expires. But once you sign a user in using signInWithCustomToken() , they will remain signed in into the device until their session is invalidated or the user signs out. |
<claims> (optional) | Optional custom claims to include in token, which can be accessed through auth.token (or request.auth.token ) in expressions. For example, if you create a custom claim adminClaim , you can access it with auth.token.adminClaim . | |
Что дальше?
- Firebase SQL Connect provides an Admin SDK to let you perform queries and mutations from privileged environments .
- Learn about IAM security in the guide for managing services and databases .