IFTTT через директивы
Gato GraphQL предоставляет возможность реализовывать стратегии IFTTT («Если это, то то») с помощью директив. Эти директивы динамически добавляются к запросу всякий раз, когда в нём присутствует какое-либо конкретное поле или директива.
В общем случае IFTTT — это правила, запускающие действия при наступлении определённого события. В нашем случае пары событие/действие выглядят следующим образом:
- Если «поле X найдено в запросе», то «прикрепить директиву Y к полю X»
- Если «директива Z найдена в запросе», то «выполнить директиву Y до/после директивы Z»
Динамическое добавление директив IFTTT к схеме является рекурсивным процессом: такая директива, в свою очередь, может иметь собственный набор директив IFTTT, которые также добавляются в цепочку директив.
Где это применяется
«Под капотом» клиенты в Gato GraphQL используют этот механизм для настройки схемы GraphQL.
Например, Access Control позволяет выбрать, какие правила контроля доступа применять к операциям, полям и директивам. Именно через IFTTT эти правила применяются к элементам схемы GraphQL.

В целом вот несколько типичных сценариев использования:
Определение max-age кэш-контроля для каждого поля отдельно
Прикрепить директиву @CacheControl ко всем полям, настроив значение параметра maxAge: 1 год для поля url типа Post и 1 час для поля title.
Настройка контроля доступа
Прикрепить директиву @validateDoesLoggedInUserHaveAnyRole к полю email типа User, чтобы запрашивать электронную почту пользователя могли только администраторы.
Синхронизация контроля доступа с кэш-контролем
Объединяя директивы в цепочку, можно гарантировать, что при проверке прав доступа пользователя к полю или директиве ответ не будет кэшироваться. Например:
- Прикрепить директиву
@validateIsUserLoggedInк полюme - Прикрепить директиву
@CacheControlсо значением аргументаmaxAgeравным0к директиве@validateIsUserLoggedIn.
Усиление безопасности
Прикрепить директиву @validateIsUserLoggedIn к директиве @translate, чтобы злоумышленники не могли выполнять queries к сервису GraphQL, способные перегрузить сервер и резко увеличить счёт (в данном случае @translate основана на Google Translate и использует платный API)
Как это работает
Как мы добавляем директивы в схему через IFTTT? Предположим, например, что мы хотим создать пользовательскую директиву @authorize(role: String!), чтобы проверять, имеет ли пользователь, выполняющий запрос к полю myPosts, ожидаемую роль author, или показывать ошибку в противном случае.
Если бы мы создавали схему с помощью SDL, она выглядела бы так:
directive @authorize(role: String!) on FIELD_DEFINITION
type User {
myPosts: [Post] @authorize(role: "author")
}Правило IFTTT определяет то же намерение, что и приведённый выше SDL: всякий раз при запросе поля myPosts выполнять директиву @authorize(role: "author") для него. Затем, каждый раз когда поле myPosts обнаруживается в запросе, движок автоматически прикрепляет @authorize(role: 'author') к этому полю в исполняемом запросе.
Правила IFTTT могут также срабатывать при обнаружении директивы, а не только поля. Например, можно настроить правило: «Всякий раз, когда в запросе обнаруживается директива @translate, выполнять директиву @cache(time: 3600) для этого поля».
Добавление директив IFTTT к запросу — рекурсивный процесс: он порождает новое событие, которое обрабатывается правилами IFTTT, что потенциально влечёт прикрепление других директив к запросу, и так далее.
Например, правило «Всякий раз, когда обнаруживается директива @cache, выполнять директиву @log» добавит запись о выполнении поля, а затем вызовет новое событие, связанное с этой только что добавленной директивой.
Настройка через PHP-код
Тип User имеет поля roles и capabilities, которые можно считать чувствительной информацией, поэтому они не должны быть доступны обычному пользователю.
Мы можем прикрепить директиву @validateDoesLoggedInUserHaveAnyRole к этим двум полям, настроив её так, чтобы доступ к ним имел только пользователь с определённой ролью (заданной через переменную окружения). Конфигурация передаётся через CompilerPass:
$accessControlManagerDefinition = $containerBuilderWrapper->getDefinition(AccessControlManagerInterface::class);
if ($roles = Environment::anyRoleLoggedInUserMustHaveToAccessRolesFields()) {
$accessControlManagerDefinition->addMethodCall(
'addEntriesForFields',
[
UserRolesAccessControlGroups::ROLES,
[
[RootObjectTypeResolver::class, 'roles', $roles],
[UserObjectTypeResolver::class, 'roles', $roles],
[RootObjectTypeResolver::class, 'capabilities', $roles],
[UserObjectTypeResolver::class, 'capabilities', $roles],
]
]
);
}
if ($capabilities = Environment::anyCapabilityLoggedInUserMustHaveToAccessRolesFields()) {
$accessControlManagerDefinition->addMethodCall(
'addEntriesForFields',
[
UserCapabilitiesAccessControlGroups::CAPABILITIES,
[
[RootObjectTypeResolver::class, 'roles', $capabilities],
[UserObjectTypeResolver::class, 'roles', $capabilities],
[RootObjectTypeResolver::class, 'capabilities', $capabilities],
[UserObjectTypeResolver::class, 'capabilities', $capabilities],
]
]
);
}При выполнении запроса пользователи, не вошедшие в систему, а также пользователи без необходимых ролей не получат доступа к этим полям.