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

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

Відкрита

1. Навіщо взагалі думати про права в ChatGPT‑App (і чим тут особливий ризик)

У «звичайному» вебзастосунку між користувачем і вашою базою даних є лише кілька шарів: фронтенд, API, БД. У ChatGPT‑App між користувачем і API з’являється ще один активний учасник — LLM. І це не просто «текстовий фільтр», а сутність, яка:

  • сама обирає, які інструменти викликати та з якими аргументами;
  • може бути обманута prompt‑інʼєкцією у даних;
  • може «переплутати» інструменти або вигадати аргументи, яких ви не очікували.

Якщо дати LLM надто багато повноважень, ви отримаєте класичну проблему Confused Deputy: модель сумлінно робить те, що, як їй здається, просить користувач або текст у документах, але водночас викликає delete_all_orders замість get_last_order.

Тому наша мета:

  1. Мінімізувати права auth_tokenʼів (які дані та дії взагалі доступні).
  2. Обмежити перелік інструментів, доступних моделі в конкретному сценарії.
  3. Додати людський контроль там, де наслідки особливо критичні.

І все це треба зробити без параної та тотальних заборон. Інакше застосунок стане непридатним. Баланс між зручністю й безпекою — головне завдання цього модуля.

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‑сервер, ми розглядаємо як «запит від користувача, сформульований моделлю». А вирішувати, чи справді можна виконати операцію, — обов’язок вашого бекенд‑коду, а не промпта.

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 — і далі додаєте цей контекст до всіх викликів інструментів.

Далі в обробнику інструмента:

// 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, ви показуєте користувачу модальне вікно зі зрозумілим формулюванням. І лише після натискання «Підтвердити» реально викликаєте інструмент.

Приклад компонента у віджеті (спрощено):

// 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 у токені. Друга велика вісь — сегментація мережі та сервісів.

Ідеальна картина:

  • у вас є рівно один публічний вхід у бекенд — MCP Gateway/Edge API;
  • усе, що зберігає персональні дані (PII) і гроші, живе в приватній мережі/VPC та доступне лише через цей шлюз;
  • вихідний трафік із бекенду обмежений списком дозволених доменів (allowlist: платіжка, CRM, свої мікросервіси).

Схематично:

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

Тут важливі кілька правил:

  1. БД і внутрішні сервіси не виставлені напряму в інтернет. Прямий доступ до них — лише з private‑мережі й тільки з тих сервісів, яким це справді потрібно.
  2. Edge/Gateway виконує автентифікацію та обмеження частоти запитів (rate limiting). Саме він перевіряє токен і scopes, відсікає надто часті запити та пише основні журнали аудиту.
  3. Egress‑контроль. MCP‑сервер не повинен уміти ходити будь‑куди в інтернет (SSRF‑атаки, витоки даних). Список зовнішніх хостів краще явно обмежити.

На практиці, якщо ви розгортаєте MCP на Vercel, Render або в Kubernetes‑кластері, частину цих речей не завжди можна налаштувати вручну. Але навіть тоді варто розділяти:

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

Підсумок: у нас уже є дві осі захисту — права в токені (scopes) і мережеві межі. Додаймо до них ще одну: багатокористувацькість (multi‑tenant), коли один і той самий App обслуговує кілька організацій.

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

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

Що таке tenant і де його брати

Tenant — це зазвичай:

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

Головна властивість: дані одного тенанта не мають бути видимі іншому.

В 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 із контексту.

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

Спробуймо простежити один сценарій від початку до кінця.

  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 з userId=123, tenantId="acme", scopes=["catalog:read","orders:write",...].
  6. createOrder усередині MCP:
    • робить requireScope(ctx, "orders:write");
    • через enforceTenant фіксує tenant;
    • створює замовлення тільки в межах tenantId="acme".
  7. Якщо при цьому замовлення вимагає миттєвої оплати, модель або сам бекенд далі ініціюють 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. У реальному продакшні так робити не можна: увесь доступ має йти через один захищений gateway/proxy, а БД — жити в приватній мережі. Інакше будь‑який знайдений уразливий endpoint або уразливий вебхук — пряма дорога до даних.

Помилка №6: Використовувати ті самі scopes/секрети в dev і prod.
Улюблений спосіб випадково видалити 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 анотації
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.