# Замена postgres superuser в food-market-server

Sprint 13, задача 1. Дата: 2026-06-07.

## Контекст

`food-market-server` — legacy backend (back.food-market.kz, port 8084
на prod-vm `192.168.1.190`, systemd `food-market-server.service`).
Хранилище — `food-market-server-postgres` (Docker, port 5436).

До этой задачи в `appsettings.Production.json` была строка с
**superuser'ом**:
```
Host=localhost;Port=5436;Database=food_market_server;Username=postgres;Password=1q2w3e4r
```

Это плохо по двум причинам:
- Слабый пароль (`1q2w3e4r`), известен любому, кто прочитает конфиг.
- `postgres` — суперюзер: CREATE DATABASE, CREATE ROLE, REPLICATION,
  BYPASS RLS, может уничтожить всё что угодно в кластере (включая
  другие БД, если они там появятся).

## Решение

Создан dedicated app-role `food_market_server_app`:
- LOGIN + сильный пароль (48 hex chars).
- NOSUPERUSER, NOCREATEDB, NOCREATEROLE, NOREPLICATION, NOBYPASSRLS.
- Гранты: SELECT/INSERT/UPDATE/DELETE на все существующие таблицы +
  USAGE/SELECT/UPDATE на sequences + USAGE/CREATE на schema public
  (CREATE нужен для EF миграций, которые app запускает на старте через
  `db.Database.Migrate()`).
- DEFAULT PRIVILEGES `FOR ROLE postgres IN SCHEMA public` — все
  будущие таблицы, что создаст superuser (например, если миграцию
  применить вручную через `postgres`), автоматически получат CRUD
  для app-роли.

## Что сделано (атомарно)

```
1. Бэкап конфига:
   /opt/food-market-server/appsettings.Production.json
     → appsettings.Production.json.bak.20260607-fms-rolemigration

2. Создание роли в БД:
   CREATE ROLE food_market_server_app LOGIN PASSWORD '...'
       NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
   GRANT CONNECT ON DATABASE food_market_server TO food_market_server_app;
   GRANT USAGE, CREATE ON SCHEMA public TO food_market_server_app;
   GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO food_market_server_app;
   GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public TO food_market_server_app;
   ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
       GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO food_market_server_app;
   ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public
       GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO food_market_server_app;

3. Обновление appsettings.Production.json:
   Username: postgres                  → food_market_server_app
   Password: 1q2w3e4r                  → <48-hex>

4. systemctl restart food-market-server  → active.
5. curl http://localhost:8084/        → 200 (SPA fallback).
6. curl https://back.food-market.kz/  → 200.
7. Логи без EF errors после старта.
```

## Проверка работоспособности

```bash
ssh nns@192.168.1.190 'sudo systemctl status food-market-server | head -5'
curl -fsS https://back.food-market.kz/ -o /dev/null -w "HTTP %{http_code}\n"
ssh nns@192.168.1.190 'sudo journalctl -u food-market-server --since "10 minutes ago" --no-pager | grep -iE "error|fail" | head'
```

## Rollback

Если что-то сломается (миграция fails, EF Errors в логах), вернуться
к старой конфигурации одной командой:

```bash
ssh nns@192.168.1.190 'sudo cp /opt/food-market-server/appsettings.Production.json.bak.20260607-fms-rolemigration /opt/food-market-server/appsettings.Production.json && sudo systemctl restart food-market-server'
curl -fsS https://back.food-market.kz/ -o /dev/null -w "HTTP %{http_code}\n"
```

Это вернёт `Username=postgres;Password=1q2w3e4r` — старый superuser.
Новая роль `food_market_server_app` остаётся в БД (это идемпотентный
`CREATE IF NOT EXISTS`), её можно дропнуть отдельно:

```sql
-- Только после успешного rollback'а и подтверждения что приложение
-- работает на postgres:
DROP OWNED BY food_market_server_app;  -- (если бы что-то создавала)
DROP ROLE food_market_server_app;
```

## Что НЕ покрыто (TODO)

- **Ротация пароля postgres**. Сам `postgres` superuser остался с тем
  же `1q2w3e4r`. Пока он не используется в работе app'а (мы только
  что переключились на app-роль) — но всё равно нужно сменить на
  сильный, иначе кто-то с доступом к dev-машине или к /opt видит
  старый бэкап и пробует. Делать через `ALTER ROLE postgres PASSWORD
  '...'` под superuser-сессией.
- **PGHBA**. Сейчас доверяется любой коннект с loopback (стандартный
  `pg_hba.conf` postgres-контейнера). Допустимо для single-host
  setup, но при сетевом расширении нужно ужесточать.
- **Audit log внутри PG** (pgaudit) — не настроен. Логирует только app.
- **Per-table RLS** — не используется, потому что приложение само
  фильтрует по `OrganizationId`. Под добавление RLS не подписывался.

## Дальше

Аналогичную замену нужно провести в:
- `food-market-stage-postgres` (port 5435) — там пользователь
  `food_market` уже без superuser-прав (см. `deploy/docker-compose.yml`,
  он создаётся через `POSTGRES_USER` env, что даёт superuser в рамках
  только этой БД — лучше чем кросс-БД superuser, но всё равно стоит
  ужесточить аналогично).
- `food-market-postgres` (prod admin.food-market.kz, port 5434) — то же.
- Локальный dev PG на host'е (brew postgresql@14) — там безразлично
  (dev-only, локальный сокет, пустой пароль работает по trust).
