Пользователь и права доступа
В предыдущем состоянии mutation принимала studentId напрямую. Теперь выделим текущего субъекта запроса и запретим запись другого студента. Пользовательские данные в input не должны самостоятельно определять полномочия. Клиент может попросить что угодно, а сервер обязан решить, допустимо ли это для того, кто обращается. Схема проверяет форму значений, но не знает автоматически корпоративное правило или связь ID с текущей личностью.
В Академии используем явную демонстрацию: заголовок X-Lab-Student со значением s01 или s02 выбирает вымышленного студента. Это не настоящий токен и не проверка личности. Любой локальный клиент может подставить такой заголовок, поэтому сервер остаётся только на loopback. Цель шага — научить передавать субъект в context и применять предметную политику; промышленный вход требует другой проверенной границы.
Контекст одного выполнения
В функции run из заголовка формируется Student либо null. Этот объект передаётся как contextValue в GraphQL.js. Все resolvers выбранного выполнения получают один контекст. Он не хранится в глобальной переменной «текущий пользователь», поэтому два параллельных запроса не должны менять субъекта друг друга. Такая локальность важнее удобства доступа к одной общей переменной: HTTP-сервер обслуживает обращения независимо.
Корневое поле me возвращает текущего студента или null. Оно нужно для чтения состояния клиента, однако не выдаёт право самостоятельно. Mutation использует context напрямую. Если клиент вообще не запросил me, серверная проверка всё равно выполняется. Не следует строить доступ как предварительный клиентский запрос «кто я», после которого браузер якобы сам решает, можно ли отправлять изменение.
if(!context.student)fail("Sign in locally","UNAUTHENTICATED");if(context.student.id!==input.studentId)fail("Cannot enroll another student","FORBIDDEN");
Это фрагмент resolver enroll из полного состояния lesson-07. Первая ветка отличает отсутствие субъекта, вторая — предложение записать другого студента. Лишь затем выполняются прежние поиск курса, проверка ID и сохранение пары. Порядок помогает не раскрывать лишние сведения через запрещённую операцию. Правильные courseId и studentId не отменяют необходимости полномочия.
Проверка до предметного изменения
Для анонимного контекста возвращается UNAUTHENTICATED. Для попытки s01 записать s02 — FORBIDDEN. Это разные причины. Клиенту в первом случае может понадобиться действительный вход, во втором — другой маршрут разрешённой операции или объяснение запрета. В учебной версии выбор заголовка остаётся демонстрацией; не следует показывать сообщение «войдите безопасно», будто такой механизм уже реализован в приложении.
Проверка находится в самой предметной mutation, а не только в HTTP-обработчике. Функцию run можно вызвать без сети, и политика останется той же. Это полезно для будущих задач или другого транспорта: новый путь выполнения не должен незаметно обойти правило. При более крупном приложении решение можно вынести в предметный сервис, который вызывается из разных resolvers, сохраняя одну проверенную границу изменения.
Сейчас input всё ещё содержит studentId. Это позволяет увидеть проверку несогласованного предложения. Более лаконичный API самостоятельной записи мог бы вообще убрать это поле и брать студента только из context. Тогда операция «записать себя» не принимает чужое значение. Однако для административной записи понадобился бы отдельный договор и разрешение. Выбор формы API должен отражать набор действий продукта, а не случайное удобство одного клиента.
Чтение и владение данными
Список курсов остаётся доступным локально без субъекта. В нашей схеме нет списка всех Enrollment, поэтому запись не становится автоматически общедоступным справочником студентов. При добавлении такого чтения потребуется отдельная политика: student видит свои записи, преподаватель — определённые курсы, администратор — больше. Нельзя считать, что защита одной mutation обеспечила все будущие поля API. Каждый путь раскрытия данных имеет собственный предметный смысл.
Также ID не является доказательством права. Даже если ключ s01:c01 трудно встретить случайно, он понятен из результата и может быть вычислен. Права не должны зависеть от секретности формата идентификатора. Для чтения конкретной записи сервер должен сопоставить объект с context и разрешением. Курс пока не добавляет этот маршрут, поэтому не заявляет защиту всех возможных операций только из-за наличия одного студента в контексте.
Согласованный повтор
Повтор enroll той же пары остаётся идемпотентным после проверки доступа. Субъект s01 может получить существующую запись s01:c01. Но другой субъект не должен получить её через запрещённый input только потому, что объект уже находится в Map. Если проверять права только при создании, ветка раннего возврата существующей записи становится обходом. В нашем порядке право проверяется до поиска сохранённого результата.
Это правило пригодится запросному кэшу следующего этапа. Кэш не должен смешивать данные, которые видят разные субъекты. Если сохранить результат глобально по одному ID и вернуть его без политики, скорость увеличится ценой раскрытия чужой информации. Контекст задаёт место для ограниченных ресурсов текущего запроса. Каждый новый ускоряющий механизм следует оценивать вместе с границей доступа, а не добавлять отдельно от неё.
Ожидаемый самостоятельный сценарий: запрос без заголовка получает me:null и отказ mutation. Со значением s01 можно записать s01 на c01, но нельзя записать s02. Со значением s02 действует аналогичный договор для второго студента. Поле авторов по-прежнему может дать частичный errors для c03; это другая причина, которая не меняет субъекта. В ходе подготовки сервер и сценарии не запускались, поэтому результат описан как ожидаемый.
При переходе к настоящему входу следует заменить функцию получения субъекта проверкой сессии либо подписанного токена, определить издателя, аудиторию, срок действия и завершение доступа. Остальные resolvers должны продолжить получать понятный предметный Student. Такой разделённый договор помогает не распространять знание транспорта по всей схеме. Следующий урок расширит чтение списка курсорной пагинацией, сохранив тот же контекст и ту же политику mutation.
Context не следует отправлять обратно целиком только ради удобства отладки. В дальнейшем он может содержать сведения сессии, внутренний загрузчик, соединение источника или разрешения, которые не предназначены клиенту. Публичное me явно выбирает допустимые поля Student. Это сохраняет ту же границу, что и Course: наличие свойства внутри программы не делает его частью схемы автоматически. Полезно удерживать договор независимо от того, насколько безобидными кажутся два вымышленных имени в первой версии примера.
Исходники урока
Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-07. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.
Первичные источники: документация 1.