# Приёмка liderra.ru — ПУНКТ №16: Изоляция (каждый видит только своё)

> **Для исполнителя:** формат PR1 (одобрен 20.06). Каждый шаг = `🔧 Код-факт (file:line)` · `🎬 Действие` · `📋 ОТЧЁТ (было→ожидали→стало + 📸/📝)` · `❌ Если не так`. Техника «под капотом»; владельцу — человеческие карточки.

**Цель:** показать, что клиенты **полностью изолированы** друг от друга: каждый видит только **свои** сделки, проекты, баланс; чужие данные не доступны **никаким способом** — ни по списку, ни по прямой ссылке (чужой номер → «не найдено»); даже когда одна заявка раздана нескольким клиентам, каждый видит **только свою копию**, не зная про других. Это основа доверия: клиент уверен, что его база контактов никому не утечёт.

**Что снимаем глазами:** **все разделы** двух кабинетов (Сделки / Проекты / Баланс) — у каждого свой набор; попытка открыть чужую сделку по ссылке → экран «не найдено». Живые 📸.

**Источник сценария:** свод проверок №16 (ISO-SECTIONS/SHARE/DIRECT/NEG/GLOBAL); R3 «движок».

---

## 🔧 Код-факты (подтверждено чтением 20.06)

- **Изоляция на уровне базы (RLS)** — middleware `SetTenantContext` оборачивает запрос в транзакцию и ставит `app.current_tenant_id` → политика Postgres фильтрует **каждую** выборку по тенанту; PgBouncer-safe через `SET LOCAL` — [SetTenantContext.php:28-49](../../../app/app/Http/Middleware/SetTenantContext.php#L28-L49).
- **Изоляция на уровне приложения** — контроллеры дополнительно ограничивают по `tenant_id` и `findOrFail` → чужой ID = **404** — например [ProjectController.php:170](../../../app/app/Http/Controllers/Api/ProjectController.php#L170), [:240](../../../app/app/Http/Controllers/Api/ProjectController.php#L240), [:248](../../../app/app/Http/Controllers/Api/ProjectController.php#L248); сделки — `where('tenant_id', …)` в [DealController::index :105-114](../../../app/app/Http/Controllers/Api/DealController.php#L105-L114) и `show`.
- **Защита от подмены тенанта** — заголовок `X-Tenant-Id` принимается **только** на dev/testing; на проде игнорируется (иначе спуфинг) — [:69-77](../../../app/app/Http/Middleware/SetTenantContext.php#L69-L77). Нет контекста тенанта → **403** — [:32-34](../../../app/app/Http/Middleware/SetTenantContext.php#L32-L34).
- **Шеринг не течёт** — одна заявка нескольким клиентам создаёт **отдельную** сделку каждому (своя строка `deals` с `tenant_id`); каждый видит только свою — RLS + per-tenant Deal (см. №1/№10).

---

## Шаг 16-1 — Клиент A видит только своё

**🔧 Код:** RLS — [SetTenantContext :28-49](../../../app/app/Http/Middleware/SetTenantContext.php#L28-L49); сделки — [DealController::index :105-114](../../../app/app/Http/Controllers/Api/DealController.php#L105-L114).
**🎬 Действие (лок):** войти клиентом A; открыть Сделки / Проекты / Баланс.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Сделки A | — | только сделки A (его число) | _(слот прогона)_ | 📸 Сделки A |
| Проекты A | — | только проекты A | _(слот)_ | 📸 Проекты A |
| Баланс A | — | баланс A | _(слот)_ | 📸 Баланс A |

**❌ Если не так:** в списках A мелькают чужие строки → утечка (СТОП).

---

## Шаг 16-2 — Клиент B видит только своё (другой набор)

**🔧 Код:** те же ссылки.
**🎬 Действие (лок):** войти клиентом B; открыть те же разделы.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Сделки B | — | только сделки B (другой набор, не как у A) | _(слот)_ | 📸 Сделки B |
| Проекты/Баланс B | — | только B | _(слот)_ | 📸 Проекты/Баланс B |
| Пересечение с A | — | нет общих строк | _(слот)_ | 📸 сравнение (разные наборы) |

**❌ Если не так:** наборы A и B пересекаются → утечка.

---

## Шаг 16-3 — Чужая ссылка → «не найдено» (прямой доступ закрыт)

**🔧 Код:** `where tenant_id + findOrFail` → 404 — [ProjectController :170](../../../app/app/Http/Controllers/Api/ProjectController.php#L170); сделки — `show` с tenant-scope.
**🎬 Действие (лок):** под клиентом B открыть по прямой ссылке **сделку/проект клиента A** (подставить чужой номер в URL).

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Чужая сделка по ссылке | — | **«не найдено»** (404), данные не показаны | _(слот)_ | 📸 экран «не найдено» |
| Чужой проект по ссылке | — | **«не найдено»** (404) | _(слот)_ | 📸 экран «не найдено» |

**💡 Что внутри:** даже если кто-то узнает номер чужой сделки и подставит его в адрес — портал ответит «не найдено», как будто этой записи для него не существует. Доступ закрыт на двух уровнях: и сама база отдаёт только твои строки, и портал отдельно проверяет, что запись твоя. Подменить «я — другой клиент» через технический заголовок на боевом нельзя.

**❌ Если не так:** чужая сделка открылась/показала данные → утечка (СТОП, критично перед продажей).

---

## Шаг 16-4 — Шеринг не течёт: общая заявка — у каждого только своя копия

**🔧 Код:** per-tenant Deal + RLS (см. №1/№10).
**🎬 Действие (лок):** влить одну заявку, которую получили и A, и B; под каждым открыть свою сделку.

**📋 ОТЧЁТ:**
| Что | Было | Ожидалось | Стало (факт) | Чем подтверждаем |
|---|---|---|---|---|
| Сделка у A | — | своя копия заявки, без упоминания B | _(слот)_ | 📸 Сделки A (эта заявка) |
| Сделка у B | — | своя копия, не видит, что её получил A | _(слот)_ | 📸 Сделки B (эта заявка) |
| Видимость «соседей» | — | никто не знает, кому ещё ушла заявка | _(слот)_ | 📸 карточки (нет данных о других получателях) |

**💡 Что внутри:** одна заявка может уйти нескольким клиентам (до 3). Но это не «общая» запись — каждому делается **отдельная** сделка в его кабинете. Клиент A не видит, что ту же заявку получил клиент B, и наоборот. Конкуренты в системе не пересекаются.

**❌ Если не так:** клиент видит, кому ещё ушла заявка / общую запись → утечка коммерческой тайны.

---

## Verification №16 (выход из пункта)
- [ ] 📸 A видит только своё (Сделки/Проекты/Баланс); B — только своё; наборы не пересекаются.
- [ ] 📸 Чужая сделка/проект по прямой ссылке → «не найдено» (404).
- [ ] 📸 Общая заявка — у A и B отдельные копии; друг про друга не знают.
- [ ] 📝 (внутр.) RLS: прямой запрос без tenant-контекста → 0 строк / 403; счётчик утечек = 0.

## 📸 vs 📝 в этом пункте
- **📸 Скриншот:** разделы обоих кабинетов, экран «не найдено» на чужой ссылке, общая заявка в двух кабинетах.
- **📝 Текст:** RLS-проверка (0 строк без контекста), 403 на спуфинг — глазами не видны.

## Грабли
- На local подмена тенанта через `X-Tenant-Id` РАЗРЕШЕНА (для тестов) — на проде закрыта; проверять «закрытость» именно как находку прод-поведения, не локального.
- Изоляция держится на двух уровнях (RLS + app-guard) — проверять оба: список (RLS) и прямую ссылку (findOrFail).
- Связка с №14 (экспорт только свой) и №26 (API только свои сделки).
