Построение гибких GraphQL API путем обращения со схемой как с базой данных


В этом посте я хотел бы рассказать о новом архитектурном паттерне для построения гибких GraphQL API. Рассматривая схему GraphQL как базу данных, вы сможете создавать гибкие GraphQL API, не зависящие от конкретного случая использования.

В настоящее время мы работаем над созданием WunderGraph Cloud,
бессерверной платформы GraphQL API с интегрированным CI/CD, аналогичной Vercel.

Наша цель — предложить наилучший возможный опыт разработчиков для создания API, с ветвлением и возможностью развертывания нескольких версий одного и того же API, просто открыв Pull Request.

Если вам интересно, вы можете подписаться на ранний доступ.

Создание WunderGraph Cloud означает, что нам придется самим создавать API. Мы приняли решение использовать WunderGraph Framework с открытым исходным кодом для создания собственных API.

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

Использование WunderGraph для создания API принципиально отличается от традиционного подхода.

Вместо того чтобы напрямую раскрывать GraphQL Layer, мы прячем его за JSON-HTTP/JSON-RPC API. Всю работу выполняет наш фреймворк.

Такая защита от прямого раскрытия слоя GraphQL имеет массу преимуществ.

Но прежде чем мы перейдем к этому, давайте обсудим традиционный подход.

Построение GraphQL API: Корневое поле вьювера

Одним из общих шаблонов, который часто встречается в GraphQL API, является корневое поле viewer. Это поле, которое возвращает текущего аутентифицированного пользователя.

Вот простой пример:

type Query {
  viewer: User!
}
type User {
  id: ID!
  name: String!
}
Вход в полноэкранный режим Выход из полноэкранного режима

Если вы отправите GraphQL-запрос, как показано ниже, он вернет текущего аутентифицированного пользователя:

query {
  viewer {
    id
    name
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Реализация поля viewer обычно выглядит следующим образом:

  1. Промежуточное ПО извлекает текущего пользователя из запроса, например, из cookie или JWT-токена.
  2. промежуточное ПО вводит объект пользователя в контекст резольвера.
  3. Резольвер поля viewer использует идентификатор пользователя из контекста для получения пользователя из базы данных.

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

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

Что если у нас нет пользователя?
Что если мы захотим посмотреть данные другого пользователя?
Что если мы не знаем, как именно будет реализована аутентификация?

Ограничения при построении GraphQL API с учетом пользовательского контекста

Иногда у вас нет пользователя. Если вы создаете фронтенд, это может быть неочевидно, но есть много случаев использования, когда у нас на самом деле нет пользователя.

Наш API может использоваться другими микросервисами. Они работают не в контексте пользователя, а в контексте сервиса.

Они могут быть аутентифицированы, но у них не будет идентификатора пользователя.

Другой пример — когда мы создаем инструмент CLI. CLI может использоваться из конвейера CI/CD.

В этом случае мы можем внедрить учетные данные сервиса в среду CI runner. Опять же, без пользователя.

Как насчет создания панели администратора для помощи пользователям?
WunderGraph Cloud позволит пользователям развертывать «проекты».

Если возникнет проблема с одним из проектов, как наша служба поддержки сможет получить доступ к данным, если ей нужно будет пройти аутентификацию в качестве пользователя?

Как видите, существует множество вариантов использования, в которых нам придется обходить корневое поле viewer. Возможным обходным решением может быть создание второго GraphQL API только для пользователей-администраторов.

Другим вариантом может быть создание полей с префиксом admin_.

Эти специальные поля предоставят вам более гибкий доступ и потребуют аутентификации в качестве пользователя admin.

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

Построение API GraphQL, не зависящих от аутентификации, на основе идеи «актеров»

Как мы уже говорили выше, WunderGraph скрывает ваш GraphQL API за слоем JSON-RPC.
Это означает, что вы можете построить свой API, не зависящий от аутентификации.

Давайте посмотрим на первую итерацию нашего API.

type Query {
    userByID(id: ID!, actorID: ID!): MaybeUser!
}
type User {
    id: ID!
    email: String!
    firstName: String!
    lastName: String!
    slug: String!
}
union MaybeUser = NotFound | User
type NotFound {
    message: String!
}
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь нет корневого поля viewer. Вместо этого у нас есть поле userByID, которое принимает второй аргумент, actorID.

Вы можете подумать, что этот API небезопасен,
поскольку он позволяет вам запрашивать любого пользователя по ID. Но это не так.

Давайте посмотрим, как можно использовать WunderGraph для реализации всех рассмотренных выше сценариев использования без ущерба для безопасности.

Давайте создадим первую операцию для просмотра пользователем своего профиля

query($currentUserID: ID! @fromClaim(name: USERID)) {
    userByID(id: $currentUserID, actorID: $currentUserID) {
        ... on User {
            id
            email
            firstName
            lastName
            slug
        }
        ... on NotFound {
            message
        }
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Эта операция использует директиву @fromClaim.

Эта директива вводит идентификатор пользователя в обе переменные запроса, причем userID и actorID одинаковы.

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

Логика реализации базового резолвера может проверить, совпадает ли actorID с userID. Если это так, то пользователю разрешается доступ к данным.

Далее, давайте создадим операцию, с помощью которой администратор сможет просмотреть профиль любого пользователя

query($userID: ID! $actorID: ID! @fromClaim(name: USERID)) {
    userByID(id: $userID, actorID: $actorID) {
        ... on User {
            id
            email
            firstName
            lastName
            slug
        }
        ... on NotFound {
            message
        }
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

В данном случае мы инжектируем только actorID из контекста пользователя.

Переменная userID может быть определена viewer API.

Чтобы эта операция работала, нам нужно расширить логику нашего резольвера.

Например, мы можем проверить в базе данных, является ли actorID (инжектированный) пользователем admin.

Далее, давайте создадим операцию, которую можно вызвать из другого микросервиса.

На самом деле, мы уже решили эту проблему.
Микросервис может использовать поток клиентских мандатов OpenID Connect для аутентификации.

Это означает, что он получит JWT-токен с утверждением sub.
Утверждение sub вводится в директиву @fromClaim(name: USERID). Это означает, что приведенная выше операция может быть вызвана из микросервиса.

Это может быть реализовано в резолвере путем проверки actorID на наличие определенного префикса, например, svc_. Если actorID начинается с svc_, мы знаем, что запрос исходит от микросервиса.

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

Наконец, давайте создадим операцию, которую можно вызвать из конвейера CI/CD.

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

Это означает, что actorID будет что-то вроде org_1234.

Обращение с нашим API как с базой данных делает его гибким и простым в обслуживании

Как вы видели, мы смогли реализовать все случаи использования с помощью одного резольвера. Нам не потребовалось создавать второй API или второй набор резольверов. Все это стало возможным благодаря тому, что мы спрятали слой GraphQL за шлюзом API WunderGraph.

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

База данных обычно не заботится о пользовательском контексте.
Это делает его очень гибким, но в то же время уязвимым.
Однако мы (надеюсь) прячем базу данных за слоем API, так что это не проблема. В WunderGraph мы применяем тот же принцип к нашему API GraphQL.

Гибкость великолепна! Мы можем писать и поддерживать меньше кода. Но есть и другое преимущество в плане безопасности:
Мы можем легко проводить аудит доступа к нашим данным.

Построение GraphQL API с actors облегчает аудит доступа к данным.

Когда каждый вызов API должен иметь актора, мы точно знаем, кто и когда имел доступ к каким данным.

Если токен доступа скомпрометирован, мы точно знаем, какие операции были вызваны с этим токеном (actorID).
Шлюз API WunderGraph может создать для нас журнал аудита.

Заключение

Мы показали схему построения гибких и безопасных GraphQL API с дополнительным преимуществом в виде простого аудита.

Надеюсь, это вдохновит вас на новые размышления о том, как строить свои GraphQL API.

Если вам интересно узнать больше о том, как мы создаем WunderGraph и как вы можете его использовать,
пожалуйста, следите за нами в twitter, linkedin или присоединяйтесь к нашему discord, чтобы получать обновления.

Оцените статью
devanswers.ru
Добавить комментарий