Merge pull request 'feat(tradein/db): seed существующих юзеров + ASCII-CHECK на username (#2557)' (#2564) from feat/tradein-users-seed into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m4s
Deploy Trade-In / build-backend (push) Successful in 1m12s
Deploy Trade-In / deploy (push) Successful in 1m23s
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m4s
Deploy Trade-In / build-backend (push) Successful in 1m12s
Deploy Trade-In / deploy (push) Successful in 1m23s
This commit is contained in:
commit
a916ceea52
1 changed files with 127 additions and 0 deletions
127
tradein-mvp/backend/data/sql/193_tradein_users_seed.sql
Normal file
127
tradein-mvp/backend/data/sql/193_tradein_users_seed.sql
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
-- Migration 193: seed существующих юзеров в tradein_users + ASCII-CHECK на username
|
||||||
|
-- (issue #2557, эпик #2549)
|
||||||
|
--
|
||||||
|
-- WHY:
|
||||||
|
-- Migration 192 создала schema (tradein_users/tradein_sessions), но без данных —
|
||||||
|
-- DB-backed auth не может заработать, пока реальные аккаунты (сейчас живущие только
|
||||||
|
-- в auth/roles.yaml + caddy/users.caddy.snippet, legacy Caddy basic-auth) не отражены
|
||||||
|
-- в таблице. Эта миграция переносит org-карту, утверждённую владельцем продукта,
|
||||||
|
-- в данные — без единого пароля (см. WHAT.2) и без Python-кода (backend wiring — #2556,
|
||||||
|
-- team-UI для проставления паролей — отдельная задача, тоже #2556).
|
||||||
|
--
|
||||||
|
-- ASCII-CHECK (deep-review #2561, обязательное требование ДО прод-данных):
|
||||||
|
-- rbac кодирует session-username через `encode("latin-1", "replace")`. Кириллические
|
||||||
|
-- логины ОДИНАКОВОЙ длины схлопываются в одну и ту же byte-строку под этой кодировкой
|
||||||
|
-- ("иванов" и "петров" оба 6 кириллических символов -> оба превращаются в одинаковую
|
||||||
|
-- строку из '?' одной длины) -> общий downstream-identity между разными людьми, общая
|
||||||
|
-- квота, взаимный IDOR (один видит сессии/данные другого). Все текущие org-логины уже
|
||||||
|
-- ASCII (admin/kopylov/praktika/userN), поэтому constraint не конфликтует с seed'ом
|
||||||
|
-- ниже; он существует, чтобы navsegda запретить будущим кириллическим логинам попасть
|
||||||
|
-- в таблицу — fail-closed на уровне схемы, а не на уровне доверия к тому, что кто-то
|
||||||
|
-- не забудет проверить в UI/API layer.
|
||||||
|
--
|
||||||
|
-- WHAT:
|
||||||
|
-- 1. ASCII-CHECK: tradein_users_username_ascii_ck CHECK (username ~ '^[A-Za-z0-9._-]{3,64}$').
|
||||||
|
-- Добавлен ДО seed-инсертов ниже для читаемости файла (CHECK — immediate constraint,
|
||||||
|
-- Postgres валидирует им и ROW-строки транзакции независимо от того, в каком месте
|
||||||
|
-- файла он объявлен относительно INSERT, так что порядок сам по себе не критичен).
|
||||||
|
-- 2. Seed — org-карта, утверждённая владельцем продукта (2026-07-30):
|
||||||
|
-- admin role=admin, manager_id=NULL, is_active=true (владелец)
|
||||||
|
-- kopylov role=manager, manager_id=NULL, is_active=true (отдельный клиент)
|
||||||
|
-- praktika role=manager, manager_id=NULL, is_active=true (ГК «Практика»)
|
||||||
|
-- user1, user3-10 role=employee, manager_id=NULL, is_active=true (свободные слоты, без org)
|
||||||
|
-- user2 role=employee, manager_id=NULL, is_active=false («Брусника», доступ
|
||||||
|
-- закрыт 2026-07-30)
|
||||||
|
-- password_hash = NULL для ВСЕХ — пароли админ проставит вручную через team-UI (#2556).
|
||||||
|
-- NULL-hash делает password-логин невозможным для этой строки, но НЕ снимает доступ:
|
||||||
|
-- в переходный период работает только legacy Caddy basic-auth fallback (dual-mode,
|
||||||
|
-- см. комментарий password_hash в 192_tradein_users_auth.sql) — никто не теряет доступ
|
||||||
|
-- из-за этой миграции.
|
||||||
|
-- display_name = 'Копылов' для kopylov (источник — auth.py::_USERNAME_PROFILE, уже
|
||||||
|
-- задокументированная фамилия). Для остальных — NULL, реальных данных нет, не выдумываем.
|
||||||
|
-- НЕ мигрируем admintest/pilottest/analysttest/expiredtest — temp QA-фикстуры
|
||||||
|
-- (auth/roles.yaml), остаются только там, в DB-backed auth не нужны.
|
||||||
|
--
|
||||||
|
-- IDEMPOTENCY:
|
||||||
|
-- - ADD CONSTRAINT через DO-блок с проверкой pg_constraint (Postgres не поддерживает
|
||||||
|
-- `ADD CONSTRAINT IF NOT EXISTS` для CHECK) — паттерн из
|
||||||
|
-- 189_account_estimate_usage_nonnegative.sql.
|
||||||
|
-- - INSERT ... ON CONFLICT (username) DO UPDATE, но НЕ безусловно: password_hash,
|
||||||
|
-- manager_id, display_name, org_name, email защищены COALESCE(текущее, EXCLUDED) —
|
||||||
|
-- если админ уже проставил пароль / назначил manager_id (team-API #2563 пишет
|
||||||
|
-- manager_id при создании сотрудника менеджером) / поменял display_name вручную
|
||||||
|
-- через team-UI (#2556) между двумя прогонами этого файла (например ручной re-apply
|
||||||
|
-- при recovery — обычный auto-apply тречит filename в _schema_migrations и не
|
||||||
|
-- запускает файл дважды на одном окружении, но scratch/staging БД такого
|
||||||
|
-- трекинга не имеют), повторный прогон НЕ должен затереть это состояние NULL-ом /
|
||||||
|
-- seed-дефолтом. Deep-review #2564 нашёл это живым багом: manager_id, назначенный
|
||||||
|
-- через #2563, тихо обнулялся повторным прогоном сида — employee выпадал из
|
||||||
|
-- `_LIST_EMPLOYEES_BY_MANAGER_SQL`, менеджер переставал видеть его в дашборде.
|
||||||
|
-- role намеренно синкается с EXCLUDED (не защищён) — это и есть источник истины
|
||||||
|
-- org-карты из этой миграции; если владелец продукта поправит эту таблицу новой
|
||||||
|
-- миграцией поверх, DO UPDATE-ветка должна донести исправление роли, а не
|
||||||
|
-- заморозить первый прогон навсегда.
|
||||||
|
-- - is_active НАМЕРЕННО отсутствует в SET (не COALESCE — колонка NOT NULL DEFAULT
|
||||||
|
-- true, COALESCE(NOT NULL, x) никогда не берёт x, это была бы мёртвая, вводящая в
|
||||||
|
-- заблуждение симметрия с password_hash/manager_id, deep-review #2564 medium).
|
||||||
|
-- Открытие/закрытие доступа (is_active) — решение владельца продукта, принимается
|
||||||
|
-- через UI (#2556), НЕ повторным прогоном этого seed-файла: после первой вставки
|
||||||
|
-- колонка сознательно «замораживается» на текущем значении в БД, seed её больше
|
||||||
|
-- не трогает.
|
||||||
|
--
|
||||||
|
-- Dependencies: 192_tradein_users_auth.sql (создаёт tradein_users, tradein_sessions).
|
||||||
|
-- Deploy order: после 192 (Foundation). Backend auth-код (login/password-verify) и
|
||||||
|
-- team-UI (#2556) — отдельные PR'ы ПОСЛЕ этой миграции (SQL-схема+данные первыми, см.
|
||||||
|
-- .claude/rules/sql.md "Migration order").
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Часть 1: ASCII-CHECK (immediate constraint — валидирует и вставляемые ниже строки).
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint
|
||||||
|
WHERE conname = 'tradein_users_username_ascii_ck'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE tradein_users
|
||||||
|
ADD CONSTRAINT tradein_users_username_ascii_ck
|
||||||
|
CHECK (username ~ '^[A-Za-z0-9._-]{3,64}$');
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMENT ON CONSTRAINT tradein_users_username_ascii_ck ON tradein_users IS
|
||||||
|
'Fail-closed защита от кириллических/не-ASCII логинов (deep-review #2561): '
|
||||||
|
'rbac кодирует session-username через encode("latin-1","replace"), не-ASCII '
|
||||||
|
'логины одинаковой длины схлопываются в общий downstream-identity (IDOR).';
|
||||||
|
|
||||||
|
-- Часть 2: seed org-карты (владелец продукта, 2026-07-30).
|
||||||
|
INSERT INTO tradein_users
|
||||||
|
(username, password_hash, role, manager_id, display_name, org_name, email, is_active)
|
||||||
|
VALUES
|
||||||
|
('admin', NULL, 'admin', NULL, NULL, NULL, NULL, true),
|
||||||
|
('kopylov', NULL, 'manager', NULL, 'Копылов', NULL, NULL, true),
|
||||||
|
('praktika', NULL, 'manager', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user1', NULL, 'employee', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user2', NULL, 'employee', NULL, NULL, NULL, NULL, false),
|
||||||
|
('user3', NULL, 'employee', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user4', NULL, 'employee', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user5', NULL, 'employee', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user6', NULL, 'employee', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user7', NULL, 'employee', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user8', NULL, 'employee', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user9', NULL, 'employee', NULL, NULL, NULL, NULL, true),
|
||||||
|
('user10', NULL, 'employee', NULL, NULL, NULL, NULL, true)
|
||||||
|
ON CONFLICT (username) DO UPDATE SET
|
||||||
|
role = EXCLUDED.role,
|
||||||
|
-- manager_id защищён COALESCE: team-API (#2563) пишет manager_id при назначении
|
||||||
|
-- сотрудника менеджером, повторный прогон seed'а не должен тихо обнулять эту связь.
|
||||||
|
manager_id = COALESCE(tradein_users.manager_id, EXCLUDED.manager_id),
|
||||||
|
password_hash = COALESCE(tradein_users.password_hash, EXCLUDED.password_hash),
|
||||||
|
display_name = COALESCE(tradein_users.display_name, EXCLUDED.display_name),
|
||||||
|
org_name = COALESCE(tradein_users.org_name, EXCLUDED.org_name),
|
||||||
|
email = COALESCE(tradein_users.email, EXCLUDED.email),
|
||||||
|
-- is_active НЕ в SET: NOT NULL DEFAULT true колонка, COALESCE был бы мёртвым кодом
|
||||||
|
-- (см. IDEMPOTENCY выше) — open/close доступа решается через UI (#2556), не seed'ом.
|
||||||
|
updated_at = now();
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Loading…
Add table
Reference in a new issue