Реализация авторизации пользователей с помощью AWS AppSync Функции конвейера.

AWS AppSync выполняет операции с полем GraphQL с помощью преобразователей, которые состоят из запроса и шаблона сопоставления ответов. Часто приложениям требуется выполнить несколько операций (много раз с несколькими источниками данных) для разрешения одного поля GraphQL. В прошлом это было возможно только путем реализации функции Lambda в качестве преобразователя, что не было идеальным рабочим процессом для многих случаев использования.

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

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

Обзор

В этом руководстве вы узнаете, как создать безопасный API AWS AppSync, который реализует авторизацию пользователей с помощью конвейерных преобразователей.

Схема доступа, которую мы реализуем, будет выглядеть следующим образом:
Есть два источника данных: одна база данных, в которой хранятся профили пользователей (включая строку, определяющую разрешения типа admin для каждого пользователя) и другая база данных, в которой хранятся сообщения в блогах пользователей.

Выполнение запроса на защищенные сообщения будет выглядеть так:

  1. Пользовательские запросы данных. Резолвер передаст запрос в первую конвейерную функцию.
  2. В шаблоне сопоставления запросов первой функции конвейера мы проверяем, принадлежат ли им данные, которые запрашивает пользователь.
    Если данные действительно принадлежат им, мы возвращаемся из первой функции конвейера и выполняем следующая функция конвейера, потому что мы знаем, что пользователь должен иметь возможность запрашивать свои собственные данные. Если данные действительно принадлежат им, мы запрашиваем их профиль из базы данных (на следующем шаге мы будем использовать данные профиля пользователя, чтобы увидеть уровень их роли) и передаем результаты запроса БД в ответ.
  3. В шаблоне сопоставления ответов мы проверяем профиль пользователя, чтобы узнать, является ли он администратором. Если они являются администратором, мы разрешаем выполнение запроса в следующей функции конвейера. Если он не администратор, мы выдаем сообщение об ошибке, что «Пользователь не авторизован для выполнения этого запроса», и останавливаем выполнение.
  4. Во второй функции конвейера мы запрашиваем данные из базы данных в запросе.
  5. В шаблоне отображения ответов мы возвращаем список сообщений из базы данных.

Начиная

Поскольку мы реализуем реальный сценарий использования аутентификации, мы установим тип авторизации API AppSync на Cognito User Pool и создадим пул пользователей Cognito для этого проекта. Если у вас уже есть пул пользователей Cognito, настроенный с помощью консоли AWS или интерфейса командной строки AWS Amplify, вы можете пропустить этот шаг.

Создание пула пользователей Cognito

Сначала мы создадим пул пользователей Cognito, посетив Консоль AWS и выполнив поиск Cognito. Здесь мы нажимаем Управление пулами пользователей, а затем нажимаем C создать пул пользователей.

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

Затем мы создадим клиент приложения, который будем использовать для входа в наш AppSync API. В левом меню нажмите Клиенты приложений и создайте новый клиент приложения. Дайте клиенту приложения имя и не забудьте снять флажок Создать секрет клиента.

Затем создайте пару пользователей, нажав Пользователи и группы, а затем нажав Создать пользователя. Мы будем использовать этих пользователей позже для входа в наш AppSync API.

Создание API AWS AppSync

Затем мы создадим API AWS AppSync. Для этого перейдите в Консоль AWS и найдите AppSync.

На панели управления AppSync нажмите Создать API. На следующем экране выберите Создать с нуля, а затем нажмите Начать. Затем дайте API имя и нажмите Создать.

Теперь, когда мы создали API, нам нужно обновить тип авторизации, чтобы он был Amazon Cognito User Pool, нажав Настройки в меню слева.

Здесь обновите тип авторизации до Amazon Cognito User Pool и выберите регион и пользовательский пул, которые мы создали ранее. Также установите действие по умолчанию РАЗРЕШИТЬ.

Создание схемы и источников данных

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

Здесь мы определим новый тип пользователя со следующими свойствами:

type User {
 id: ID!
 name: String!
 level: String!
}

Прокрутите вниз и нажмите Создать. Это создаст дополнительную схему для типа Пользователь, включая запросы и изменения для большинства операций CRUD, а также преобразователи и источник данных (таблица DynamoDB).

Затем мы снова нажмем Создать ресурсы и создадим набор ресурсов для другого типа сообщений:

type Post {
 id: ID!
 userId: ID!
 content: String!
}

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

Во-первых, нам нужно обновить тип CreatePostInput, чтобы не требовать userId в качестве аргумента. Поскольку пользователь аутентифицирован, мы сможем прочитать его личность в шаблоне сопоставления запроса:

input CreatePostInput {
 content: String!
}

Затем мы обновим запрос listPosts, чтобы получить новый аргумент userId. Это связано с тем, что теперь мы хотим выполнить авторизацию по этому запросу и разрешить только администраторам или владельцам данных выполнять запрос.
Позже мы ' Я также буду обновлять преобразователь этого запроса, чтобы получать только те данные, которые были созданы с помощью userId пользователя, запрашивающего данные.

listPosts(
  userId: ID!,
  filter: TablePostFilterInput,
  limit: Int,
  nextToken: String
): PostConnection

Теперь наша схема готова к работе. Нам нужно в следующий раз обновить несколько резолверов.

Обновление резолверов

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

Чтобы обновить преобразователь, нажмите «Схема» и найдите имя преобразователя в представлении преобразователя.

Мы обновим шаблон сопоставления запросов преобразователя createUser следующим образом:

{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "id": $util.dynamodb.toDynamoDBJson($context.identity.sub),
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input)
}

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

Затем мы обновим шаблон сопоставления запросов преобразователя createPost следующим образом:

$util.qr($ctx.args.input.put("userId", $ctx.identity.sub))
{
  "version": "2017-02-28",
  "operation": "PutItem",
  "key": {
    "id": $util.dynamodb.toDynamoDBJson($util.autoId()),
  },
  "attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input)
}

Этот новый преобразователь установит поле с userId сообщения, установленным на их идентификатор ID.

Наконец, мы обновим преобразователь listPosts, чтобы он стал преобразователем конвейера.

Создание конвейерного преобразователя

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

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

Перед шаблоном сопоставления

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

Здесь мы сначала изменим шаблон Перед сопоставлением на следующее:

$util.qr($ctx.stash.put("callerId", $ctx.identity.sub))
$util.qr($ctx.stash.put("userId", $ctx.args.userId))
{}

$ ctx.stash - это карта, которая доступна в каждом преобразователе и шаблоне сопоставления функций. Один и тот же экземпляр тайника переживает одно выполнение распознавателя. Это означает, что вы можете использовать тайник для передачи произвольных данных между шаблонами сопоставления запросов и ответов, а также между функциями в преобразователе конвейера.

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

Конвейерная функция 1 - источник данных таблицы пользователей

Затем мы можем нажать «Функции» в левом меню и создать нашу первую функцию.

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

Мы обновим шаблон сопоставления запросов этой функции, чтобы он выглядел следующим образом:

#if($ctx.stash.callerId == $ctx.stash.userId)
  #return($ctx.prev.result)
#else
{
    "operation": "GetItem",
    "key": {
        "id": $util.dynamodb.toDynamoDBJson($ctx.stash.callerId),
    }
}
#end

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

Директива #return(data: Object) пригодится, если вам нужно преждевременно вернуться из любого шаблона сопоставления. #return(data: Object) аналогично ключевому слову return в языках программирования, потому что оно возвращается из ближайшего блока логики с ограниченной областью видимости. Это означает, что использование #return внутри шаблона сопоставления преобразователя возвращается из преобразователя.

Мы обновим шаблон сопоставления ответов этой функции, чтобы он выглядел следующим образом:

#if($ctx.result.level != "admin")
  $util.error("User is not authorized to make this query")
#else
  $util.toJson($ctx.result)
#end

Это проверяет, является ли вызывающий этот запрос администратором. Если это так, мы продолжаем выполнение в следующей конвейерной функции. Если нет, мы выдаем ошибку.

Конвейерная функция 2 - источник данных таблицы публикаций

В шаблоне сопоставления запросов второй функции конвейера мы будем сканировать таблицу сообщений и возвращать элементы, созданные пользователем, для которого мы запрашиваем:

{
    "operation" : "Scan",
    "filter" : {
        "expression" : "#userId = :userId",
        "expressionNames" : {
            "#userId" : "userId"
        },
        "expressionValues" : {
            ":userId" : $util.dynamodb.toDynamoDBJson($ctx.stash.userId)
        }
    },
    "nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.nextToken, null))
}

В шаблоне сопоставления ответов этой функции мы просто возвращаем результаты сканирования:

#if($ctx.error)
  $util.error($ctx.error.message, $ctx.error.type)
#end
{
    "items": $util.toJson($ctx.result.items),
    "nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))
}

Тестируем это

Сначала мы войдем в систему, используя клиент приложения, который мы создали при создании пула пользователей Cognito, и создадим пользователя

mutation createUser {
  createUser(input: {
    name: "John"
    level: "admin"
  }) {
    id name level
  }
}

Затем мы создадим пару сообщений с использованием этого пользователя

mutation createPost {
  createPost(input: {
    content: "My Second Cool Post"
  }) {
    id userId content
  }
}

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

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

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

Чтобы узнать больше о функциях AWS AppSync Pipeline, ознакомьтесь с документацией здесь.

Меня зовут Надер Дабит.
Я адвокат разработчиков в AWS Mobile, работаю с такими проектами, как AWS AppSync и AWS Amplify, автор React Native in Action , и редактор React Native Training и OpenGraphQL.