Документация

К списку статей
25. Онбординг арендатора и управление пользователями

Аудитория: суперадминистратор, интегратор. Закрывает вопрос: «как завести новую организацию (арендатора) и операторов для входа — без пересборки образа?». Связано: 21 — Вход и роли, 09-tenant-onboarding (seed-путь).


Два пути завести арендатора
ПутьКогдаКак
Seed (в образе)штатные/эталонные арендаторы продукта (Артек, РГГУ)запись в backend/app/seed_data.py + init_db.py, пересборка (см. 09)
Runtime (в БД)боевой/ad-hoc арендатор у заказчика, без пересборкиадмин-вставка в БД портала + загрузка ПО через коннектор (ст. 20)

Ниже — runtime-путь (им заведён демо-арендатор «Моё рабочее место»). Ядро/экраны/контракт не меняются — изоляция (RLS tenant_isolation) работает «из коробки».

Записи делаются под админ-подключением к БД портала (ADMIN_DATABASE_URL, суперпользователь — обходит RLS). Хеш пароля — Argon2id через app.password.hash_password (Стрибог тут НЕ применяется).

Шаг 1. Создать арендатора (БД портала)
INSERT INTO sam.tenants (tenant_id, name, kind, domain, branding)
VALUES ('acme', 'ООО «Акме»', 'customer', 'acme.local',
        '{"shortName":"Акме","accent":"#1F6C9F","logoSvg":null}');

-- KPI-факты обязательны (NOT NULL); реальные суммы или 0 на старте:
INSERT INTO sam.tenant_facts (tenant_id, direct_savings_per_year, prevented_costs)
VALUES ('acme', 0, 0);

-- Хотя бы один отдел:
INSERT INTO sam.departments (dept_id, tenant_id, dept_name, dept_code, vlan_segment)
VALUES (1, 'acme', 'Рабочие места', 'ARM', 'vlan-arm');

kind: `customer` (вход по паролю) или demo_public (анонимный read-only, без операторов).

Шаг 2. Завести операторов (вход по паролю)

Операторы заводятся через API (роль superadmin, только в пределах своего арендатора — кросс-арендаторные операции запрещены, 403). Пароль хешируется на сервере (Argon2id) — сырой пароль наружу не сохраняется. Эндпоинты (см. ст. 21 о входе/CSRF):

МетодПутьНазначение
GET/api/{tenant}/usersсписок операторов (без passwordHash)
POST/api/{tenant}/usersсоздать оператора (409 при дубле логина)
POST/api/{tenant}/users/{login}/passwordсбросить пароль
POST/api/{tenant}/users/{login}/activeвключить/отключить вход
# создать superadmin@acme (нужен токен superadmin этого арендатора)
curl -sX POST https://<host>/api/acme/users \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"login":"superadmin@acme","displayName":"Администратор","role":"superadmin","password":"СИЛЬНЫЙ-ПАРОЛЬ"}'
  • Логин — формат `роль@арендатор` (it_admin@acme).
  • Роли: viewer, it_manager, it_admin, dpo, superadmin (+ служебная collector для форвардера).

Минимальная длина пароля — 8 символов; недопустимая роль/короткий пароль → 400.

  • Заведите минимум superadmin@acme (полный доступ) и нужные рабочие роли.
Курица-и-яйцо: первого superadmin@acme создать через API нельзя (нет токена этого арендатора). Заведите его одним из путей: скриптом ниже (контур api/enclave), либо разовой SQL-вставкой под ADMIN_DATABASE_URL. Дальше — управляйте операторами через API.
# add_user.py — bootstrap первого superadmin: docker compose ... exec -T -w /app api python add_user.py
import os, psycopg
from app.password import hash_password

TENANT, LOGIN, ROLE, PASSWORD = "acme", "superadmin@acme", "superadmin", "СИЛЬНЫЙ-ПАРОЛЬ"
pwh = hash_password(PASSWORD)
with psycopg.connect(os.environ["ADMIN_DATABASE_URL"]) as c, c.cursor() as cur:
    cur.execute(
        "INSERT INTO sam.app_users (user_login, tenant_id, display_name, role, password_hash) "
        "VALUES (%s,%s,%s,%s,%s) "
        "ON CONFLICT (tenant_id, user_login) DO UPDATE SET password_hash=EXCLUDED.password_hash",
        (LOGIN, TENANT, "Администратор", ROLE, pwh))
Сброс пароля / деактивация
# сброс пароля
curl -sX POST https://<host>/api/acme/users/viewer@acme/password \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"password":"НОВЫЙ-ПАРОЛЬ"}'
# отключить вход (is_active=false)
curl -sX POST https://<host>/api/acme/users/viewer@acme/active \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"active":false}'

is_active=false отключает вход (логин-роут это проверяет). Чтобы немедленно оборвать ВСЕ уже активные сессии (всех арендаторов) — сменить JWT_SECRET (см. ст. 21).

Шаг 3. Наполнить ПО

Инвентарь/лицензии — не вручную, а через коннектор (KSC/1С/syslog, ст. 20) или разовый импорт (как _gen_pc_import.py для демо). После наполнения: REFRESH MATERIALIZED VIEW sam.mv_license_utilization;


Проверка (чек-лист)
  1. Арендатор в списке: GET /api/tenants содержит acme (в режиме DEMO_MODE=1) — или вводится

вручную на входе (DEMO_MODE=0, см. 21).

  1. Вход superadmin@acme проходит, попадаете в контур acme.
  2. Изоляция: по токену acme данные других арендаторов недоступны (RLS).
  3. GET /api/acme/dashboard-kpi отвечает (нули — норма до наполнения).
Грабли
  • `tenant_facts` обязателен (NOT NULL) — без него дашборд части арендатора упадёт.
  • `demo_public` не имеет операторов и read-only — для входа по паролю нужен customer.
  • Пароли только хешем (Argon2id) — не храните и не логируйте сырой пароль.

Связанные: 21 — Вход и роли · 20 — Коннекторы · 07 — RBAC.