Архитектура
АрхитектураIFTTT через директивы

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],
      ]
    ]
  );
}

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