JavaRush /Курсы /ChatGPT Apps /Контроль доступа и минимизация прав: scopes, сегментация,...

Контроль доступа и минимизация прав: scopes, сегментация, per-tool permissions

ChatGPT Apps
15 уровень , 0 лекция
Открыта

1. Зачем вообще думать о правах в ChatGPT‑App (и чем тут особенный риск)

В «обычном» веб‑приложении между пользователем и вашей базой всего пара слоёв: фронтенд, API, БД. В ChatGPT‑App между пользователем и API появляется ещё один активный участник — LLM. И это не просто «текстовый фильтр», а сущность, которая:

  • сама выбирает, какие инструменты вызывать и с какими аргументами;
  • может быть обманута prompt‑инъекцией в данных;
  • может «перепутать» инструменты или придумать аргументы, которых вы не ожидали.

Если дать LLM слишком много полномочий, вы получаете классическую проблему Confused Deputy: модель добросовестно выполняет то, что, как ей кажется, просит пользователь или текст в документах, но при этом вызывает delete_all_orders вместо get_last_order.

Поэтому наша цель:

  1. Минимизировать права auth_token'ов (какие данные и действия доступны вообще).
  2. Ограничить, какие инструменты вообще доступны модели в конкретном сценарии.
  3. Добавить человеческий контроль там, где последствия особенно критичны.

И всё это надо сделать без паранойи и тотального запрета всего, иначе App станет бесполезным. Баланс между удобством и безопасностью — наш главный квест в этом модуле.

2. Модель доступа в экосистеме: кто к чему лезет

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

flowchart TD
  U[Пользователь в ChatGPT] --> C[ChatGPT UI + LLM]
  C --> A["Ваш App (визуальный план + виджет)"]
  A --> G[MCP Gateway / API Edge]
  G --> S[MCP-серверы и микросервисы]
  S --> D[Базы данных, очереди, внешние API]

Кратко по ролям:

  • ChatGPT UI и LLM: управляются OpenAI. Вы задаёте им инструкции (system‑prompt, tool descriptions), но не контролируете внутренние токены и права платформы.
  • Ваш App (план, tools, виджет): вы решаете, какие инструменты доступны, как они описаны, какие UX‑подтверждения нужны, какие данные виджет может отображать.
  • MCP Gateway / API Edge: здесь происходит проверка токена, маппинг userId, tenantId, списка scopes и маршрутизация на нужный сервис.
  • MCP‑серверы и микросервисы: выполняют инструменты, делают запросы к БД и внешним API. Здесь должны быть максимально строгие проверки: scopes, tenant‑изоляция, валидация входных данных.
  • Хранилища и внешние API: последняя линия обороны (ограничения на уровне БД, прав учёток внешних сервисов).

Ключевая мысль: LLM не является источником прав доступа. Всё, что приходит в MCP‑сервер, мы рассматриваем как «запрос от пользователя, сформулированный моделью». Решать, можно ли действительно выполнять операцию, — обязанность вашего backend‑кода, а не промпта.

3. AuthN vs AuthZ: что мы уже умеем и что добавляем

В модуле про аутентификацию вы уже делали:

  • AuthN (Authentication) — выясняли, кто этот пользователь. Через OAuth 2.1/PKCE ChatGPT получал от IdP токен, который потом прикладывался к вызовам MCP. В нём были sub, user_id или аналог, иногда tenant_id.
  • Базовый AuthZ — возможно, вы уже умели разделять роли user/admin и проверяли хотя бы «юзер ли это» или «админ».

Теперь усложняем картину:

  • каждый auth_token должен нести набор scopes — строковые права вида resource:action, например catalog:read, orders:write, payments:create;
  • ваш MCP‑сервер должен проверять соответствие этих scopes каждому действию, а не только «один раз на входе»;
  • разные инструменты и даже разные операции внутри одного инструмента могут требовать разных scopes.

В терминах OAuth 2.1 ChatGPT — это «public client», MCP — «resource server», а ваш OAuth‑сервер знает, какие scopes поддерживаются и что именно они означают. Метаданные MCP‑ресурса обычно объявляют scopes_supported, чтобы ChatGPT мог запрашивать у пользователя ровно нужные разрешения.

4. Проектируем scopes для GiftGenius

Возьмём наш учебный GiftGenius и посмотрим, какие у него есть домены данных и действий. Из функционала у нас что‑то вроде:

  • просмотр каталога и карточек подарков;
  • рекомендации на основе истории;
  • создание заказов;
  • запуск чекаута / списание средств;
  • админское редактирование каталога.

Вместо того чтобы делать один всесильный giftgenius:full_access, лучше разложить это на разумные scopes.

Соглашение по именованию: resource:action

Хорошо работает стратегия resource:action, где:

  • resource описывает домен: catalog, recommendations, orders, payments, admin.
  • action описывает тип действия: read, write, иногда более конкретно: create, delete, manage.

Пример для GiftGenius:

Scope Что разрешает
catalog:read
Читать публичный каталог подарков
recommendations:read
Читать историю рекомендаций пользователя
orders:write
Создавать новые заказы
orders:read
Читать историю заказов пользователя
payments:create
Инициировать платёж / чек-аут
catalog:admin
Редактировать каталог (только для админ‑UI/поддержки)

Обычный пользователь GiftGenius потребует что‑то вроде (перечисляются через пробел): catalog:read recommendations:read orders:write orders:read payments:create. Администратору добавим catalog:admin.

Важно: не делаем универсальный *:* или admin:all. Чем более гранулярно, тем проще потом отозвать конкретное право, не ломая всё приложение целиком.

Типы scopes: read vs write vs critical

Полезно мысленно пометить scopes категориями:

  • безопасные (read): не меняют состояние, максимум раскрывают данные;
  • мутирующие (write): создают/меняют сущности, накручивают стойки, но не трогают деньги и не удаляют всё подряд;
  • критичные (critical): платежи, удаление аккаунта, массовое удаление данных.

Для критичных прав можно применять повышенный контроль:

  • выдавать их минимальному числу пользователей;
  • запрашивать у пользователя отдельное согласие в UI ChatGPT при выдаче токена;
  • на стороне MCP требовать дополнительного подтверждения (например, одноразовый PIN, но это уже продвинутые сценарии).

Scopes в коде: RequestContext и requireScope

На уровне MCP удобно завести единый тип контекста:

// mcp/context.ts
export interface RequestContext {
  userId: string;        // кто
  tenantId: string;      // в рамках какой организации
  scopes: string[];      // какие права выданы токену
}

// Простой хелпер для проверки прав
export function requireScope(
  ctx: RequestContext,
  needed: string
) {
  if (!ctx.scopes.includes(needed)) {
    throw new Error(`Missing scope: ${needed}`);
  }
}

Предполагается, что RequestContext вы формируете в MCP Gateway после валидации токена: раскодировали JWT, проверили подпись/истечение, вытащили sub, tenant, scope — и дальше прикладываете этот контекст ко всем вызовам инструментов.

Дальше в tool‑хендлере:

// mcp/tools/createOrder.ts
import { requireScope, RequestContext } from "../context";

export async function createOrder(
  input: CreateOrderInput,
  ctx: RequestContext
) {
  requireScope(ctx, "orders:write");
  // дальше - логика создания заказа
}

Теперь, даже если модель внезапно вызовет createOrder там, где по UX вы этого не ожидали, без orders:write инструмент просто не выполнится.

securitySchemes на уровне инструмента

MCP‑спецификация позволяет каждому инструменту указывать, какие схемы авторизации и scopes ему нужны. В официальных примерах securitySchemes прикручиваются прямо к описанию инструмента.

Условный пример:

// mcp/server.ts
server.registerTool(
  "createOrder",
  {
    title: "Create order",
    description: "Creates a new order for current user",
    inputSchema: {/*...*/},
    securitySchemes: [
      { type: "oauth2", scopes: ["orders:write"] }
    ]
  },
  async ({ input }, ctx: RequestContext) => {
    requireScope(ctx, "orders:write");
    // ...
  }
);

Здесь два уровня защиты:

  • декларативный: ChatGPT знает, что для этого инструмента нужен orders:write и при отсутствии прав будет инициировать auth‑флоу (или сообщит об этом пользователю);
  • императивный: ваш код ещё раз всё проверяет перед реальным действием.

Если токен есть, но не хватает scopes, сервер должен вернуть ошибку с WWW-Authenticate: Bearer error="insufficient_scope", scope="orders:write" — и ChatGPT сможет запросить у пользователя расширение прав (step‑up authorization).

Insight

В официальных примерах используется securitySchemes. Она не была утверждена в официальной спецификации в том виде, в котором написана в примерах ChatGPT Apps SDK. Поэтому её нужно помечать как расширение официального протокола — оборачивать в _meta. Рабочий вариант примера выше:

// mcp/server.ts
server.registerTool(
  "createOrder",
  {
    title: "Create order",
    description: "Creates a new order for current user",
    inputSchema: {/*...*/},
    _meta: {										// вот так
      securitySchemes: [
        { type: "oauth2", scopes: ["orders:write"] }
      ]          
    }
  },
  async ({ input }, ctx: RequestContext) => {
    requireScope(ctx, "orders:write");
    // ...
  }
);

5. Per‑tool permissions и «опасные» инструменты

Scopes отвечают на вопрос «что в принципе может делать этот auth_token». Но внутри токена есть ещё и список инструментов, которыми модель может пользоваться. Их тоже надо проектировать аккуратно.

Классификация инструментов

Условно делим инструменты на:

  • информационные (informational / read‑only): читают данные, строят отчёты, считают что‑то без побочных эффектов;
  • действующие (consequential): изменяют состояние, списывают деньги, что‑то удаляют.

В документации по ChatGPT Apps прямо рекомендуют для read‑only инструментов явно помечать, что они безопасны, а для опасных — описывать последствия и включать дополнительные UX‑подтверждения.

Это можно делать:

  • через аннотации к инструменту (условные поля вроде readOnlyHint, destructiveHint);
  • через текстовое описание: «Этот инструмент удаляет заказы безвозвратно»;
  • через отдельный флаг confirmation_required, который ваш App‑план использует, чтобы вставить шаг‑подтверждение в диалог.

UX‑подтверждения для критичных действий

Например, у GiftGenius есть инструмент chargeCustomer (инициирует списание средств). Вы, разумеется, не хотите, чтобы модель вызывала его без согласия пользователя.

Как это может выглядеть на уровне плана App:

// app/plan/tools.ts (псевдокод)
export const tools = [
  {
    name: "giftgenius.list_catalog",
    description: "Показать каталог подарков",
    annotations: { readOnlyHint: true }
  },
  {
    name: "giftgenius.create_order",
    description: "Создать заказ без оплаты",
    annotations: { consequential: true }
  },
  {
    name: "giftgenius.charge_customer",
    description: "Списать деньги за заказ",
    annotations: {
      consequential: true,
      destructiveHint: true,
      confirmationRequired: {
        title: "Списать деньги с карты?",
        message: "Будет проведён платёж по заказу N."
      }
    }
  }
];

Конкретные имена полей зависят от версии SDK, но сама идея совпадает с рекомендациями: read‑only инструменты помечаем как безопасные, опасные — как требующие явного подтверждения и хорошего объяснения в описании.

Дальше ваш виджет может отреагировать: если модель предлагает вызвать charge_customer, вы показываете пользователю модальное окно с понятной формулировкой и только после клика «Подтвердить» реально делаете tool‑call.

Пример компонентки в виджете (упрощённо):

// widget/components/ConfirmCharge.tsx
export function ConfirmCharge(props: {
  orderId: string;
  onConfirm: () => void;
}) {
  return (
    <div>
      <p>Списать деньги за заказ {props.orderId}?</p>
      <button onClick={props.onConfirm}>
        Да, подтвердить оплату
      </button>
    </div>
  );
}

Модель инициирует идею «пора платить», но финальную кнопку нажимает человек. Это и есть human‑in‑the‑loop, который так любят безопасники.

Инструменты только для агентов/бэк‑офиса

Ещё один частый кейс: у вас есть инструменты, которыми могут пользоваться только агенты (в смысле Agents SDK) или внутренние админки, но не «обычный» ChatGPT App пользователя.

Например, rebuildSearchIndex или syncCatalogFromERP. Их лучше:

  • не включать в общий список tools для обычного App;
  • настроить в отдельном агенте/оркестраторе;
  • защитить отдельными scopes и, возможно, отдельным Auth‑контуром.

Если просто добавить их в список доступных инструментов App, вы повышаете риск, что модель внезапно решит: «А давай я пересоздам индекс прямо сейчас, вдруг поможет найти подарок».

6. Сетевая сегментация и границы доверия

Права — это не только scopes на токене. Вторая крупная ось — сегментация сети и сервисов.

Идеальная картина:

  • у вас есть ровно один публичный вход в backend — MCP Gateway/Edge API;
  • всё, что хранит PII и деньги, живёт в приватной сети/VPC и доступно только через этот шлюз;
  • outbound‑трафик с backend ограничен списком разрешённых доменов (allowlist: платёжка, CRM, свои микросервисы).

Схематично:

flowchart LR
  ChatGPT -- HTTPS --> Edge[API Gateway / MCP Endpoint]
  Edge -- private network --> MCP[MCP server]
  MCP -- private --> DB[(DB с PII)]
  MCP -- private --> SVC[Internal microservices]
  MCP -- HTTPS (allow) --> Stripe[Payments API]

Здесь важны несколько правил:

  1. DB и внутренние сервисы не выставлены напрямую в интернет. Прямой доступ к ним только из private‑сети и только с тех сервисов, которым это действительно нужно.
  2. Edge/Gateway проводит auth и rate‑limiting. Именно он проверяет токен и scopes, режет слишком частые запросы и пишет основные audit‑логи.
  3. Egress‑контроль. MCP‑сервер не должен уметь ходить по любым URL из интернета (SSRF‑атаки, утечки данных). Список внешних хостов лучше явно ограничить.

На практике, если вы деплоите MCP на Vercel, Render или в Kubernetes‑кластере, часть этих вещей руками не настраивается, но даже там можно разделять:

  • отдельные проекты/кластеры для dev/staging/prod;
  • разные переменные окружения и ключи для каждого окружения;
  • отдельный «edge» сервис (HTTP‑обёртка MCP) и отдельные приватные сервисы.

Итого, у нас уже есть две оси защиты: права на токене (scopes) и границы сети. Добавим к ним ещё одну — многотенантность, когда одно и то же App обслуживает несколько организаций.

7. Multi‑tenant / организационный контекст

До сих пор мы мысленно работали с одним пользователем. Но многие ChatGPT‑приложения — мультиарендные (multi‑tenant): один и тот же App обслуживает десятки компаний. GiftGenius легко превратить в B2B‑сервис для корпораций: у каждого отдела свои каталоги, бюджеты, заказы.

Что такое tenant и где его брать

Tenant — это обычно:

  • организация/компания (Acme Corp);
  • рабочее пространство (workspace);
  • иногда проект или среда.

Главное свойство: данные одного tenant’а не должны быть видны другому.

В auth‑флоу tenant обычно кладут в:

  • claim токена (tenant, org_id);
  • отдельный параметр в запросе авторизации (но это менее надёжно, чем claim, подписанный IdP).

Важно: доверяем только tenantId из проверенного токена, а не из аргументов инструментов. Если модель сгенерирует {"tenantId": "acme"}, а в токене у пользователя tenantId: "globex", это должно восприниматься как попытка взлома.

Tenant в контексте запроса

Добавим tenantId в наш RequestContext (мы уже это сделали выше) и не будем позволять его переопределять из входных данных.

Базовая проверка:

// mcp/tenant.ts
import { RequestContext } from "./context";

export function enforceTenant<TInput>(
  input: TInput & { tenantId?: string },
  ctx: RequestContext
) {
  if (input.tenantId && input.tenantId !== ctx.tenantId) {
    throw new Error("Tenant mismatch");
  }
  return { ...input, tenantId: ctx.tenantId };
}

Дальше в инструменте:

// mcp/tools/listOrders.ts
export async function listOrders(
  input: { limit?: number; tenantId?: string },
  ctx: RequestContext
) {
  const safe = enforceTenant(input, ctx);
  return db.order.findMany({
    where: { tenantId: safe.tenantId },
    take: safe.limit ?? 20
  });
}

Мы игнорируем tenant из аргументов и жёстко подставляем его из контекста. Так даже если LLM или злоумышленник пытается «подсунуть» чужой tenant, ничего не выйдет.

Tenant‑изоляция на уровне БД

Архитектурно есть разные варианты:

  • отдельная БД на tenant;
  • отдельные схемы;
  • одна БД с tenant_id в каждой таблице и строгой фильтрацией.

Какой бы вариант вы ни выбрали, золотое правило одно: ни один запрос к БД не должен выполняться без фильтрации по tenant_id из контекста. Особенно это важно в RAG/векторном поиске: если забыть фильтр по tenant, модель может начать искать по документам чужих организаций.

8. Как это связывается с нашим Next.js/Apps SDK приложением

Давайте теперь соберём всё это вместе и посмотрим, как scopes, tenant и сетевые границы приземляются в наш Next.js‑проект на Apps SDK. Давайте добавим больше конкретики и посмотрим на код Next.js и Apps SDK.

Где живут scopes и tenant в нашем проекте

Типичный расклад для учебного проекта:

  • В Next.js‑приложении (Apps SDK) у вас есть конфиг App/коннектора и странички для OAuth callback’ов.
  • В MCP‑сервере — код, который принимает HTTP/SSE‑запросы из ChatGPT, проверяет токен и вызывает нужный инструмент.

Переносим туда всё, что обсудили:

  1. В OAuth‑настройках MCP‑ресурса объявляем scopes_supported для GiftGenius (catalog:read, orders:write, и т.п.).
  2. В Apps SDK‑конфиге описываем App с перечислением инструментов и их аннотациями (read‑only, consequential, confirmation‑flows).
  3. В MCP‑сервере реализуем:
    • парсинг и проверку токена;
    • формирование RequestContext { userId, tenantId, scopes };
    • хелперы requireScope, enforceTenant и т.п.;
    • вызовы БД всегда через tenantId из контекста.

Пример «изолированного» пути для создания заказа

Попробуем проследить один сценарий end‑to‑end.

  1. Пользователь пишет: «Оформи заказ на этот набор для бюджета 50$».
  2. Модель решает, что нужно вызвать giftgenius.create_order с аргументами { productId, budget, ... }.
  3. ChatGPT проверяет: есть ли у App инструмент create_order, какие для него scopes и securitySchemes прописаны. Понимает, что нужен orders:write.
  4. Если токен уже есть и содержит orders:write, запрос идёт дальше; если нет — ChatGPT инициирует OAuth‑авторизацию с запросом нужного scope.
  5. MCP Gateway принимает запрос, проверяет токен, формирует RequestContext c userId=123, tenantId="acme", scopes=["catalog:read","orders:write",...].
  6. createOrder внутри MCP:
    • делает requireScope(ctx, "orders:write");
    • через enforceTenant фиксирует tenant;
    • создаёт заказ только в рамках tenantId="acme".
  7. Если при этом заказ требует мгновенной оплаты, модель или сам backend далее инициируют charge_customer, где:
    • инструмент в плане помечен как confirmationRequired;
    • виджет рендерит ConfirmCharge и просит пользователя явно подтвердить списание.

Так мы получаем глубинную защиту: слишком широкие промпты, prompt‑инъекции или даже баги в UX не приведут к неконтролируемым действиям, потому что внизу пирамиды всё равно стоят строгие проверки scopes, tenant и ручные подтверждения для критичных действий.

9. Типичные ошибки при проектировании прав и сегментации

Ошибка №1: Один толстый scope вроде app:full_access.
Такой подход удобен на демо, но опасен в продакшене. Потеряли один токен — потеряли всё. Нельзя отозвать или запретить одну операцию, не сломав остальные. Разбивайте права по доменам и типам операций (read/write/critical).

Ошибка №2: Проверять права только «на входе» и не проверять внутри инструментов.
Иногда делают так: «раз ChatGPT получил токен, то он уже всё умеет». А дальше инструмент createOrder просто вызывается, даже если для этого конкретного токена не было выдано orders:write. Правильный подход — проверять scopes в каждом инструменте (или хотя бы централизованным middleware для всех мутирующих операций).

Ошибка №3: Не маркировать опасные инструменты и не требовать подтверждения.
Если инструмент списывает деньги, удаляет данные или меняет доступы, он не должен выглядеть для модели так же, как listCatalog. Отсутствие явных аннотаций и UX‑подтверждений повышает шанс, что модель вызовет его «просто потому что логично». Как минимум, разделяйте read‑only и destructive‑инструменты и явно помечайте вторые.

Ошибка №4: Доверять tenantId из аргументов инструмента.
Очень частый анти‑паттерн: инструмент getOrders({ tenantId }), где tenantId приходит от модели. Если использовать его как есть, пользователь из tenantA может получить доступ к данным tenantB, просто указав другой идентификатор. Tenant должен приходить из проверенного токена и навязываться всем запросам к БД и внешним сервисам, а пользовательские значения либо игнорируются, либо валидируются на совпадение.

Ошибка №5: MCP/БД доступны из интернета напрямую.
Иногда в простых прототипах MCP‑сервер и БД торчат в интернет на голом HTTP/5432. В prod‑мире так делать нельзя: весь доступ должен идти через один защищённый gateway/proxy, а БД жить в приватной сети. Иначе любой найденный уязвимый endpoint или дырявый вебхук — прямая дорога к данным.

Ошибка №6: Использование одних и тех же scopes/секретов в dev и prod.
Любимый способ устроить себе внезапное удаление прод‑данных во время демонстрации локального dev‑окружения. Для каждого окружения должны быть свои ключи, scopes и БД. Даже если кто‑то получит доступ к dev‑токену, он не сможет навредить prod‑данным.

Ошибка №7: Нежелание «отказывать модели».
Иногда разработчики переживают: «Если я буду часто возвращать ошибки insufficient_scope или forbidden, модель будет работать хуже». На практике это нормальное и ожидаемое поведение: модель учится, какие действия ей доступны, а какие требуют дополнительных прав или подтверждения. Хуже, если она «успешно» делает то, что делать не должна — например, проводит второй платёж.

1
Задача
ChatGPT Apps, 15 уровень, 0 лекция
Недоступна
Минимальный requireScope для одного MCP-инструмента
Минимальный requireScope для одного MCP-инструмента
1
Задача
ChatGPT Apps, 15 уровень, 0 лекция
Недоступна
Per-tool permissions: securitySchemes + readOnly/destructive аннотации
Per-tool permissions: securitySchemes + readOnly/destructive аннотации
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ