SIRINVIDEO CCTV Документация администратора и пользователя На сайт 2026.08

Организации и пользователи

Организации и квоты, пользователи и роли, гранты доступа, смена пароля, эффективные права.

Раздел описывает управление арендаторами и доступом: организации с их квотами, учётные записи, роли, гранты (правила доступа к камерам) и теги, по которым эти гранты выдаются.

Во всех примерах используются переменные:

BASE=https://cctv.example.com
TOKEN=$(curl -sS -X POST "$BASE/api/v1/auth/login" \
  -H 'Content-Type: application/json' \
  -d '{"login":"admin","password":"ВАШ-ПАРОЛЬ"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')

ORG_ID=<UUID>
USER_ID=<UUID>
TAG_ID=<UUID>

Общие соглашения — заголовки, коды ошибок, пагинация, вход и обновление сессии — в Введении и аутентификации. Те же операции через интерфейс описаны в разделах Организации, квоты и лимиты и Пользователи и права доступа.

Две вещи, на которых чаще всего спотыкаются
  1. Заголовок Content-Type: application/json обязателен при любом теле запроса — без него запрос отклоняется до обработчика с кодом 415 и текстовым (не JSON) телом.
  2. Пустое значение числового параметра — ошибка, а не «параметр не задан»: ?limit= даёт 400 Failed to deserialize query string. Ненужный параметр не отправляйте вовсе.

Модель доступа#

Прежде чем создавать пользователей, разберитесь с тремя понятиями: роль, грант и эффективные права. Роль задаёт, что человек может администрировать; грант — что он может смотреть; эффективные права — результат вычисления гранта на конкретной камере.

Три роли#

Роль Значение в API Что может
Суперадминистратор superadmin вся установка: ноды, организации, все пользователи и камеры. Доступ к объектам вытекает из роли, гранты ему не нужны
Администратор организации org_admin всё внутри своей организации: камеры, пользователи, теги, мозаики. Чужая организация — 403 {"error":"cross_org"}
Оператор user только то, что выдано грантами. Без грантов не видит ни одной камеры

Правила, которые система соблюдает жёстко:

  • суперадминистратор существует только в организации по умолчанию;
  • хотя бы один включённый суперадминистратор должен существовать всегда: последнего нельзя удалить, отключить или понизить в роли (409 {"error":"last_superadmin"});
  • администратор организации не может ни создать суперадминистратора, ни изменить его (403 {"error":"superadmin_is_superadmin_only"}), ни выйти за пределы своей организации.

Роль и организация читаются из базы при каждом запросе, а не из токена. Понижение в роли или отзыв сессии действуют со следующего же запроса.

Что такое грант#

Грант — это пара «область + маска привилегий». Область отвечает на вопрос «на какие камеры», маска — «что с ними можно делать».

Область Как задаётся Смысл
Вся организация поле base привилегии на все камеры организации
Тег элемент массива tags с tag_id привилегии на все камеры, помеченные этим тегом
Одна камера тег, назначенный ровно одной камере отдельного «гранта на камеру» в API нет: сделайте тег на одну камеру и выдайте грант на него

Маска привилегий — целое число, сумма битов:

Привилегия Значение бита Что открывает
Живое видео 1 просмотр живого потока
Архив 2 воспроизведение записи DVR
События 4 лента событий по камере
PTZ 8 поворот, наклон, зум
Экспорт 16 выгрузка фрагментов архива
Превью 32 статичные кадры-превью

Примеры: «живое видео + архив» — 1 + 2 = 3; «живое видео + архив + события + превью» — 1 + 2 + 4 + 32 = 39; все привилегии — 63. Допустимая маска — строго больше 0 и меньше 64.

Гранты выдаются только операторам

Попытка сохранить гранты администратору или суперадминистратору отклоняется: 409 {"error":"grants_user_role_only"}. Их доступ вытекает из роли, и грант ничего бы не добавил.

Отдельно от грантов существуют ссылки для внешних получателей — временный доступ к одной камере без учётной записи; они описаны в разделе Доступ по ссылке.

Как считаются эффективные права#

У каждой камеры есть набор меток: идентификатор её организации плюс идентификаторы всех её тегов. Эффективная маска камеры для пользователя — побитовое ИЛИ всех масок, чьи метки совпали хотя бы с одной меткой камеры. Это объединение, а не пересечение: если один грант дал живое видео, а другой — архив, человек получит и то и другое.

Суперадминистратору доступ выдаётся универсальной маской 63 на всю установку, администратору организации — служебным грантом «вся своя организация, маска 63». Гранты в базе им не нужны.

Точно так же считает и нода, поэтому ответ GET /api/v1/users/{id}/effective показывает не приблизительную оценку, а то, что нода реально разрешит.

Пример: оператор с доступом «живое видео + архив» на один тег#

Полный сценарий из четырёх вызовов.

1. Создайте тег и пометьте им нужные камеры (пометка камеры — в разделе Настройка камер):

TAG_ID=$(curl -sS -X POST "$BASE/api/v1/tags" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Проходная","kind":"site"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
echo "$TAG_ID"

2. Создайте оператора (роль user — значение по умолчанию):

USER_ID=$(curl -sS -X POST "$BASE/api/v1/users" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"login":"operator1","password":"ПАРОЛЬ-НЕ-МЕНЕЕ-8-СИМВОЛОВ","display_name":"Оператор смены"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')

3. Выдайте грант «живое видео + архив» (1 + 2 = 3) на этот тег и ничего больше:

curl -sS -X PUT "$BASE/api/v1/users/$USER_ID/grants" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"base\":0,\"tags\":[{\"tag_id\":\"$TAG_ID\",\"privileges\":3}]}"
# {"base":0,"tags":[{"tag_id":"<UUID>","privileges":3}]}

"base": 0 означает «прав на организацию целиком не давать» — доступ будет только по тегу.

4. Проверьте результат:

curl -sS "$BASE/api/v1/users/$USER_ID/effective" -H "Authorization: Bearer $TOKEN"
# {"total_cameras":120,"live":8,"dvr":8,"events":0,"ptz":0,"export":0,"preview":0}

Читается так: в организации 120 камер, оператор видит живое видео и архив по 8 камерам с тегом «Проходная», и не имеет ни событий, ни PTZ, ни экспорта, ни превью. Если live равен нулю — скорее всего тегом не помечена ни одна камера.

Организации#

Организация — корень аренды: пользователи, камеры, теги, шаблоны, квоты и глубина хранения событий принадлежат ей. Все изменения организаций доступны только суперадминистратору; администратор организации может лишь прочитать карточку своей организации, чтобы видеть действующие квоты.

Во всех полях-лимитах значение null означает «без ограничения», а не ноль. Ноль означает «ничего нельзя». Отрицательные значения отклоняются.

GET /api/v1/orgs#

Суперадмин

Справочник организаций.

Параметры запроса

Параметр Тип По умолчанию Правила
after uuid курсор: вернуть записи с id больше указанного
limit integer 500 приводится к диапазону 1…500; отсутствующий, нулевой или отрицательный даёт полную страницу в 500 строк
q string подстрока в названии без учёта регистра, не длиннее 128 символов; символы % и _ здесь ищутся буквально
curl -sS "$BASE/api/v1/orgs?limit=50" -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

Ответ — страница { "rows": [...], "next": "<UUID>|null" }. Поле next заполняется, только если страница пришла ровно на limit строк; когда оно null — записи кончились.

Ошибки: 400 {"error":"query_too_long"} (q длиннее 128 символов).

POST /api/v1/orgs#

Суперадмин

Создаёт организацию. В одной транзакции заводятся сама организация, её счётчики использования и первая ревизия политики хранения событий.

Тело запроса. Обязательно только name, остальное имеет значения по умолчанию.

Поле Тип По умолчанию Смысл
name string обязательно непустое после обрезки пробелов, уникальное
disabled bool false пользователи отключённой организации не могут войти
max_cameras int | null null предел числа камер (null — без ограничения)
max_users int | null null предел числа учётных записей
max_sessions_per_user int | null null предел одновременных сессий одного пользователя
max_dashboards, max_reports, max_tags, max_event_templates int | null null пределы объектов аналитики, тегов и шаблонов событий
max_agents, max_agent_devices, max_agent_access_rules int | null null пределы для агентов туннелей
attachment_quota_bytes int64 | null null квота на вложения событий, в байтах
event_retention_days int | null null90 глубина хранения событий, не меньше 1
placement_mode string | null nullmanual manual, auto или force_auto — как выбирается нода для новой камеры
allowed_node_ids uuid[] | null null список разрешённых нод (null — любые)
allowed_storage_names string[] | null null список разрешённых хранилищ (null — любые)
curl -sS -X POST "$BASE/api/v1/orgs" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Филиал Север","max_cameras":200,"max_users":25,"event_retention_days":180}' \
  | python3 -m json.tool

Ответ — 201 с карточкой организации (см. следующий метод).

Ошибки

Код Тело Причина
400 {"error":"name_empty"} пустое название
400 max_cameras_negative, max_users_negative, max_sessions_negative, max_dashboards_negative, max_reports_negative, max_tags_negative, max_event_templates_negative, max_agents_negative, max_agent_devices_negative, max_agent_access_rules_negative отрицательный лимит: «без ограничения» — это null, а не минус
400 {"error":"attachment_quota_negative"} отрицательная квота вложений
400 {"error":"retention_days_min"} event_retention_days меньше 1
400 {"error":"retention_days_unrepresentable"} слишком большая глубина хранения
400 {"error":"bad_placement_mode"} режим размещения вне списка
409 {"error":"already_exists"} название занято

GET /api/v1/orgs/{id}#

Администратор — суперадмин видит любую организацию, админ организации только свою (иначе 403 cross_org).

Карточка организации вместе со счётчиками использования.

curl -sS "$BASE/api/v1/orgs/$ORG_ID" -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
{ "id": "<UUID>", "name": "Филиал Север", "disabled": false,
  "max_cameras": 200, "max_users": 25, "max_sessions_per_user": null,
  "max_dashboards": null, "max_reports": null, "max_tags": null,
  "max_event_templates": null, "max_agents": null, "max_agent_devices": null,
  "max_agent_access_rules": null, "attachment_quota_bytes": null,
  "event_retention_days": 180, "placement_mode": "manual",
  "allowed_node_ids": null, "allowed_storage_names": null,
  "deleting_at": null, "created_at": "2026-01-01T00:00:00Z",
  "usage": { "attachment_bytes": 0, "cameras_count": 12, "users_count": 3,
             "dashboards_count": 0, "reports_count": 0, "tags_count": 4,
             "event_templates_count": 0, "agents_count": 0, "agent_devices_count": 0,
             "agent_access_rules_count": 0, "events_count": 0, "attachments_count": 0 } }

Счётчики usage поддерживаются транзакционно и не пересчитываются при чтении, поэтому метод дёшев. Непустое deleting_at означает, что организация уже поставлена в очередь на удаление.

Ошибки: 403 {"error":"cross_org"}; 404 {"error":"not_found"}.

PATCH /api/v1/orgs/{id}#

Суперадминадминистратор организации не может править собственную организацию.

Частичное изменение: название, признак отключения, квоты, глубина хранения событий и политика размещения. Изменение выполняется под блокировкой строки, поэтому одновременные правки не теряют полей. Все поля необязательны, пропущенное поле остаётся прежним.

curl -sS -X PATCH "$BASE/api/v1/orgs/$ORG_ID" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"max_cameras":300,"placement_mode":"auto","allowed_node_ids":null}'

Ошибки: те же коды 400, что при создании, плюс 404 not_found и 409 already_exists.

Числовую квоту нельзя вернуть в «без ограничения» этим методом

Передача "max_cameras": null не снимает лимит: значение null для числовых квот неотличимо от «поле не передано», и текущее значение сохраняется. Так ведут себя все поля max_* и attachment_quota_bytes.

Снять null-ом можно только два поля — allowed_node_ids и allowed_storage_names (null вернёт «любые ноды» и «любые хранилища»).

Практический обход: задайте заведомо большое число вместо «без ограничения», либо снимите лимит через интерфейс администратора. Значение 0 не подходит: это «ничего нельзя».

Изменение event_retention_days добавляет новую ревизию политики хранения — прошлые события продолжают жить по той политике, при которой были записаны.

GET /api/v1/orgs/{id}/deletion-preview#

Суперадмин

Показывает, что именно будет уничтожено при удалении организации. Ничего не меняет.

curl -sS "$BASE/api/v1/orgs/$ORG_ID/deletion-preview" -H "Authorization: Bearer $TOKEN" \
  | python3 -m json.tool
{ "org_id": "<UUID>", "name": "Филиал Север",
  "users": 3, "cameras": 12, "tags": 4, "event_templates": 0, "camera_templates": 1,
  "reports": 0, "dashboards": 0, "widgets": 0, "mosaics": 2, "module_tokens": 0,
  "agents": 0, "agent_devices": 0, "agent_endpoint_leases": 0, "agent_access_rules": 0,
  "events": 15230, "attachments": 412, "attachment_bytes": 918273645,
  "audit_rows": 8801, "linked_database_rows": 0,
  "dvr_files": 20418, "dvr_bytes": 4398046511104, "dvr_telemetry_stale": false }

Признак dvr_telemetry_stale: true означает, что цифры по архиву взяты из несвежей телеметрии нод — считайте dvr_files и dvr_bytes приблизительными.

Ошибки: 404 {"error":"not_found"}.

DELETE /api/v1/orgs/{id}#

Суперадмин

Ставит организацию в очередь на уничтожение. Это не мгновенный каскад: Plane делает снимок предпросмотра, немедленно замораживает организацию (disabled: true, проставляется deleting_at) и ставит долговременное задание, которое постепенно вычищает события, вложения и архив, и только затем удаляет саму запись.

JOB_ID=$(curl -sS -X DELETE "$BASE/api/v1/orgs/$ORG_ID" -H "Authorization: Bearer $TOKEN" \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')

curl -sS "$BASE/api/v1/cleanup-jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

Ответ — 202 с заданием: id, kind, org_id, state, preview (замороженные цифры), progress, created_at, finished_at, last_error.

Ошибки

Код Тело Причина
409 {"error":"default_org_undeletable"} организацию по умолчанию удалить нельзя никогда
409 {"error":"organization_deleting"} удаление уже запущено
404 {"error":"not_found"} нет такой организации
Удаление организации необратимо и начинается немедленно

Организация замораживается в тот же миг: её пользователи больше не войдут, даже если задание ещё не доработало. Отменить запущенное удаление нельзя.

Всегда сначала вызовите GET /api/v1/orgs/{id}/deletion-preview и покажите его цифры тому, кто принимает решение: сколько камер, событий, вложений и сколько байт архива будет уничтожено. Ответ на DELETE — это задание, а не удалённая организация; следите за ним через GET /api/v1/cleanup-jobs/{id} (только суперадмин; списка заданий нет — сохраните id). Задание завершено, когда finished_at не null; поле last_error показывает неудачу.

POST /api/v1/orgs/{id}/dvr-purge/preview#

Суперадмин

Пробный прогон очистки архива: сколько камер, файлов и байт попадёт под выборку. Ничего не меняет.

Тело запроса. Все поля необязательны, поэтому {} — это «все камеры за всё время до текущего момента».

Поле Тип По умолчанию Смысл
camera_ids uuid[] [] пусто — все камеры организации; не более 1000 элементов, дубликаты отбрасываются
from RFC 3339 | null null нижняя граница включительно; null — с самого начала
to RFC 3339 | null null верхняя граница; null и любое будущее время приводятся к «сейчас»
curl -sS -X POST "$BASE/api/v1/orgs/$ORG_ID/dvr-purge/preview" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"from":"2026-01-01T00:00:00Z","to":"2026-02-01T00:00:00Z"}' | python3 -m json.tool
{ "spec": { "camera_ids": [], "from": "2026-01-01T00:00:00Z", "to": "2026-02-01T00:00:00Z" },
  "preview": { "cameras": 12, "dvr_files": 3120, "dvr_bytes": 671088640000,
               "dvr_telemetry_stale": false, "range_size_exact": false } }

В ответе возвращается нормализованная выборка (spec) — с отброшенными дубликатами и приведённой верхней границей. Для последующего удаления отправляйте именно её, а не исходную.

Признак range_size_exact: true бывает только тогда, когда не заданы ни from, ни to. Для выборки с границами по времени цифры файлов и байт — это текущие итоги камер, то есть контекст, а не точная оценка.

Ошибки

Код Тело Причина
400 {"error":"dvr_purge_selection_too_large"} больше 1000 камер в выборке
400 {"error":"bad_time_range"} после нормализации from не меньше to
400 {"error":"dvr_camera_not_in_organization"} указана камера чужой организации
404 {"error":"not_found"} нет такой организации

POST /api/v1/orgs/{id}/dvr-purge#

Суперадмин

Ставит в очередь фактическое удаление архива по той же выборке. Plane делает свежий предпросмотр и запускает долговременное задание, которое удаляет записи на нодах-владельцах и в настроенной копии S3.

Тело запроса — то же, что у предпросмотра.

JOB_ID=$(curl -sS -X POST "$BASE/api/v1/orgs/$ORG_ID/dvr-purge" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"from":"2026-01-01T00:00:00Z","to":"2026-02-01T00:00:00Z"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')

# опрос выполнения
curl -sS "$BASE/api/v1/cleanup-jobs/$JOB_ID" -H "Authorization: Bearer $TOKEN" \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["state"],d["finished_at"],d["last_error"])'

Ответ — 202 с заданием того же вида, что при удалении организации.

Ошибки: все ошибки предпросмотра, плюс:

Код Тело Причина
409 {"error":"organization_deleting"} организация уже удаляется
409 {"error":"dvr_purge_empty"} в выборке нет ни одной камеры с настроенной записью архива
Очистка архива необратима — сначала предпросмотр

Удалённые записи не восстанавливаются: ни на ноде, ни в копии S3. Всегда вызывайте POST /api/v1/orgs/{id}/dvr-purge/preview с тем же телом и сверяйте cameras, dvr_files и dvr_bytes перед запуском.

Удаление выполняется асинхронно: метод возвращает задание (202), а не результат. Следите за ним через GET /api/v1/cleanup-jobs/{id} — задание закончено, когда finished_at не null. Списка заданий нет, сохраняйте id из ответа.

Верхняя граница выборки замораживается на момент запроса, поэтому записи, сделанные после нажатия, уже не будут удалены — это защита от «догоняющей» очистки. Камеры без настроенного хранилища архива из выборки исключаются; если таких камер была вся выборка, вернётся 409 dvr_purge_empty.

Пользователи#

Учётная запись принадлежит одной организации. Логин — глобальная идентичность установки: он уникален по всей системе и после создания не меняется.

GET /api/v1/users#

Администратор — суперадмин видит все организации или сужает выборку через ?org=, админ организации закреплён за своей.

Список учётных записей. Поиск и фильтры выполняются в базе до ограничения по числу строк, поэтому искать можно по всей организации, а не по загруженной странице.

Параметры запроса

Параметр Тип По умолчанию Правила
org uuid сужение для суперадмина; чужая организация для остальных — 403 cross_org
after uuid курсор постраничного обхода
limit integer 500 приводится к диапазону 1…500
q string подстрока по login или display_name без учёта регистра
role string superadmin, org_admin или user; неизвестное значение — ошибка разбора, а не «фильтр не применён»
disabled bool true/false; без параметра возвращаются и включённые, и отключённые
curl -sS "$BASE/api/v1/users?role=user&limit=100" -H "Authorization: Bearer $TOKEN" \
  | python3 -m json.tool
{ "rows": [ { "id": "<UUID>", "org_id": "<UUID>", "login": "operator1", "role": "user",
              "display_name": "Оператор смены", "locale": "ru", "disabled": false,
              "created_at": "2026-01-01T00:00:00Z", "last_login_at": null } ],
  "next": null }

Хеш пароля не входит ни в один ответ API.

Ошибки: 403 {"error":"cross_org"}; 403 {"error":"not_admin"}.

В поиске по пользователям % и _ работают как подстановочные знаки

Строка q на этом методе подставляется в шаблон поиска без экранирования. Текст, введённый человеком, может неожиданно расширить выборку: % заменяет любую последовательность символов, _ — один символ. Учитывайте это, если передаёте q из формы напрямую.

POST /api/v1/users#

Администратор — суперадмин может создать учётную запись в любой организации, админ организации только в своей.

Создаёт пользователя. Квота max_users проверяется под блокировкой счётчика, поэтому одновременные создания не могут её превысить.

Тело запроса

Поле Тип По умолчанию Правила
login string обязательно непустой после обрезки пробелов, уникален по всей установке, не меняется потом
password string обязательно минимум 8 символов; требований к составу нет
role string user superadmin, org_admin или user
org_id uuid своя организация суперадмин — любая; админ организации обязан не указывать или указать свою
display_name string "" имя для человека, ни на что не влияет, но попадает в аудит
locale string | null ru язык интерфейса
curl -sS -X POST "$BASE/api/v1/users" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"login":"operator1","password":"ПАРОЛЬ-НЕ-МЕНЕЕ-8-СИМВОЛОВ","role":"user","display_name":"Оператор смены"}' \
  | python3 -m json.tool

Ответ — 201 с карточкой пользователя.

Ошибки

Код Тело Причина
400 {"error":"login_empty"} пустой логин
400 {"error":"password_too_short"} меньше 8 символов
403 {"error":"cross_org"} админ организации указал чужую организацию
403 {"error":"cannot_create_superadmin"} админ организации пытается создать суперадминистратора
409 {"error":"superadmin_default_org_only"} суперадминистратор вне организации по умолчанию
404 {"error":"not_found"} указанной организации не существует
409 {"error":"user_limit"} достигнут max_users организации
409 {"error":"already_exists"} логин занят

GET /api/v1/users/{id}#

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

curl -sS "$BASE/api/v1/users/$USER_ID" -H "Authorization: Bearer $TOKEN" | python3 -m json.tool

Ошибки: 403 {"error":"cross_org"}; 403 {"error":"superadmin_is_superadmin_only"}; 404 {"error":"not_found"}.

PATCH /api/v1/users/{id}#

Администратор — перевод в другую организацию доступен только суперадмину.

Частичное изменение. Все поля необязательны; пропущенное остаётся прежним. Поля login в теле нет — логин неизменяем.

Поле Тип Смысл
role string superadmin, org_admin, user
display_name string имя для человека
locale string язык интерфейса
disabled bool отключить или включить учётную запись
org_id uuid перевод в другую организацию — только суперадмин
curl -sS -X PATCH "$BASE/api/v1/users/$USER_ID" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"disabled":true}'

Ошибки

Код Тело Причина
404 {"error":"not_found"} нет такого пользователя либо целевой организации
403 {"error":"cross_org"} пользователь в другой организации
403 {"error":"superadmin_is_superadmin_only"} админ организации трогает суперадминистратора
403 {"error":"organization_reassignment_superadmin_only"} не-суперадмин прислал org_id вообще, даже прежний
409 {"error":"superadmin_default_org_only"} повышение вне организации по умолчанию либо любой перевод суперадминистратора: сначала понизьте роль, потом переводите
409 {"error":"user_limit"} в организации назначения достигнут max_users
409 {"error":"last_superadmin"} изменение оставило бы систему без включённого суперадминистратора
Изменение роли или организации стирает все гранты пользователя

Это не побочный эффект, а защита: понижение в роли никогда не должно «воскрешать» старые привилегии. После смены роли или организации выдайте гранты заново.

Кроме того, изменение role, org_id или переход в disabled:

  • отзывает все действующие сессии пользователя — ему придётся войти заново, а ноды перестанут принимать его прежний токен;
  • исключает его из аудиторий аналитических панелей; панель, оставшаяся без получателей, становится видимой только администраторам.

Перевод в другую организацию пишет две записи аудита — в исходной и в целевой организации.

DELETE /api/v1/users/{id}#

Администратор — суперадминистратора может удалить только суперадмин.

Удаляет учётную запись вместе с её грантами и сессиями. Идентификаторы ещё живых сессий заносятся в публичный список отзыва до удаления, поэтому нода не сможет продолжать отдавать видео по уже выданному токену.

curl -sS -X DELETE "$BASE/api/v1/users/$USER_ID" -H "Authorization: Bearer $TOKEN"
# {"id":"<UUID>","deleted":true}

Ответ — 200 с телом (не 204).

Ошибки: 404 not_found; 403 cross_org; 403 superadmin_is_superadmin_only; 409 {"error":"last_superadmin"} — в том числе при удалении отключённого суперадминистратора, если он последний.

POST /api/v1/users/{id}/password#

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

Устанавливает пароль. Проверки прежнего пароля нет: метод одинаково обслуживает и самостоятельную смену, и сброс администратором.

Тело запроса: {"password": "<не менее 8 символов>"}.

curl -sS -X POST "$BASE/api/v1/users/$USER_ID/password" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"password":"НОВЫЙ-ПАРОЛЬ-8-ПЛЮС"}'
# {"id":"<UUID>","password":"updated"}

Ошибки: 400 {"error":"password_too_short"}; 404 not_found; 403 cross_org; 403 superadmin_is_superadmin_only.

Смена пароля отзывает все сессии — включая вашу собственную

Если вы меняете пароль себе, текущий токен немедленно перестаёт работать: следующий же запрос вернёт 401. Получите новый токен методом входа. Отозванные идентификаторы расходятся по нодам через список отзыва, поэтому доступ к видео закрывается тоже.

GET /api/v1/users/{id}/grants#

Любой вошедший — свои гранты читает любой, чужие только администратор в пределах своей организации.

Возвращает сохранённый набор грантов: маску на всю организацию и маски по тегам.

curl -sS "$BASE/api/v1/users/$USER_ID/grants" -H "Authorization: Bearer $TOKEN"
{ "base": 0, "tags": [ { "tag_id": "<UUID>", "privileges": 3 } ] }

Значение base: 0 означает «гранта на организацию целиком нет», а не «привилегии не настроены».

Ошибки: 404 not_found; 403 cross_org; 403 superadmin_is_superadmin_only.

PUT /api/v1/users/{id}/grants#

Администратор — выдать грант самому себе нельзя.

Полностью заменяет набор грантов пользователя одной транзакцией. Это не слияние: всё, чего нет в теле запроса, удаляется. Тело {} очищает гранты полностью.

Тело запроса

Поле Тип По умолчанию Правила
base int32 0 маска на всю организацию; 0 — грант не создаётся, иначе строго больше 0 и меньше 64
tags array [] элементы {"tag_id": "<UUID>", "privileges": <маска>}; каждая маска строго больше 0 и меньше 64

Проверки выполняются в таком порядке: корректность каждой маски → tag_id не может быть идентификатором организации (это метка базового гранта) → запрет повторов tag_id → суммарно не более 128 строк → каждый тег должен существовать в организации этого пользователя.

curl -sS -X PUT "$BASE/api/v1/users/$USER_ID/grants" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d "{\"base\":0,\"tags\":[{\"tag_id\":\"$TAG_ID\",\"privileges\":3}]}"

Ответ — 200 с принятым набором (эхо запроса, а не повторное чтение из базы).

Ошибки

Код Тело Причина
409 {"error":"grants_user_role_only"} получатель — не оператор: администраторам гранты не выдаются
400 {"error":"bad_base_mask"} base не ноль и вне диапазона
400 {"error":"bad_tag_mask"} маска тега вне диапазона
400 {"error":"tag_id_is_org"} в tags указан идентификатор организации
400 {"error":"duplicate_tag_grant"} один и тот же тег дважды
400 {"error":"unknown_tag"} тега нет в организации получателя
409 {"error":"grant_limit"} больше 128 строк грантов
404 {"error":"not_found"} нет такого пользователя
403 cross_org, superadmin_is_superadmin_only выход за пределы своей организации
Сохранение грантов отзывает сессии получателя

Иначе снятая привилегия продолжала бы работать на видео до истечения ранее выданного токена. Оператор увидит требование войти заново — это ожидаемое поведение, а не ошибка.

GET /api/v1/users/{id}/effective#

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

Показывает, до скольких камер пользователь действительно дотягивается по каждой привилегии. Вычисляется ровно так же, как это делает нода при выдаче видео, поэтому цифрам можно верить.

curl -sS "$BASE/api/v1/users/$USER_ID/effective" -H "Authorization: Bearer $TOKEN"
{ "total_cameras": 120, "live": 8, "dvr": 8, "events": 0, "ptz": 0, "export": 0, "preview": 0 }

Поле total_cameras — размер пространства поиска: для суперадминистратора это все камеры установки, для остальных — камеры их организации.

Ошибки: 404 not_found; 403 cross_org; 403 superadmin_is_superadmin_only.

Пользуйтесь этим методом как проверкой после каждого изменения грантов — он отвечает на вопрос «человек уже видит то, что должен?» без входа под его учётной записью.

Теги#

Тег — единица, по которой выдаются гранты и собираются мозаики и отчёты. Его идентичность — UUID, поэтому переименование ничего не ломает: гранты, камеры и мозаики остаются связанными.

Виды тегов — plain, site, custom; они отличаются только оформлением в интерфейсе. Жёсткий предел — 1024 тега на организацию; организация может задать меньший max_tags, и тогда действует меньшее из двух значений.

GET /api/v1/tags#

Администратор — суперадмин по всем организациям или с ?org=, админ организации только по своей.

Параметры запроса: org (uuid), after (uuid), limit (по умолчанию 500, диапазон 1…500), q (подстрока в названии), kind (plain, site или custom; иное значение — 400 bad_kind).

curl -sS "$BASE/api/v1/tags?limit=100" -H "Authorization: Bearer $TOKEN" | python3 -m json.tool
{ "rows": [ { "id": "<UUID>", "org_id": "<UUID>", "name": "Проходная",
              "kind": "site", "created_at": "2026-01-01T00:00:00Z" } ],
  "next": null }

Ошибки: 400 {"error":"bad_kind"}; 403 cross_org; 403 not_admin.

Отдельного метода чтения одного тега нет: GET /api/v1/tags/{id} вернёт 405. Найдите тег в списке — например, с параметром q.

POST /api/v1/tags#

Администратор — суперадмин может создать тег в любой организации.

Тело запроса

Поле Тип По умолчанию Правила
name string обязательно непустое после обрезки; уникально в пределах организации
kind string | null plain plain, site или custom
org_id uuid | null своя организация чужая — только для суперадмина
curl -sS -X POST "$BASE/api/v1/tags" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Проходная","kind":"site"}'

Ошибки: 400 name_empty; 400 bad_kind; 403 cross_org; 403 not_admin; 404 not_found (нет такой организации); 409 {"error":"organization_deleting"}; 409 {"error":"tag_limit"}; 409 {"error":"already_exists"}.

PATCH /api/v1/tags/{id}#

Администратор — перевод тега в другую организацию доступен только суперадмину.

Переименовывает тег, меняет его вид или переносит в другую организацию. Гранты, привязки камер и ссылки из мозаик переименование переживают без изменений.

Тело запроса: name, kind, org_id — все поля необязательны.

curl -sS -X PATCH "$BASE/api/v1/tags/$TAG_ID" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Проходная (север)"}'

Ошибки

Код Тело Причина
404 {"error":"not_found"} нет такого тега
403 {"error":"cross_org"} тег в другой организации
403 {"error":"organization_reassignment_superadmin_only"} не-суперадмин прислал org_id вообще, даже прежний
400 name_empty, bad_kind проверка значений
409 {"error":"tag_name_exists"} в целевой организации такое имя занято
409 {"error":"tag_org_move_referenced"} тег ещё используется камерой, грантом, мозаикой или шаблоном камеры — сначала снимите ссылки
409 tag_limit, organization_deleting ограничения организации назначения

DELETE /api/v1/tags/{id}#

Администратор — в пределах своей организации.

curl -sS -X DELETE "$BASE/api/v1/tags/$TAG_ID" -H "Authorization: Bearer $TOKEN"
# {"id":"<UUID>","deleted":true}

Ошибки: 404 not_found; 403 cross_org.

Удаление тега молча забирает доступ у операторов

В отличие от переноса в другую организацию, удаление не проверяет, используется ли тег. Вместе с тегом удаляются:

  • привязки тега ко всем камерам;
  • все гранты, выданные на этот тег — оператор, у которого доступ был только по нему, теряет камеры немедленно и без предупреждения.

Перед удалением найдите тех, кто им пользуется: пройдите по операторам организации методом GET /api/v1/users/{id}/grants и убедитесь, что этот tag_id не является для кого-то единственным источником доступа. После удаления восстановить гранты можно только вручную.

Сессии#

Вход и продление сессии описаны в Введении и аутентификации (POST /api/v1/auth/login и POST /api/v1/session/refresh). Здесь важно другое: какие действия из этого раздела обрывают чужие сессии.

Действие Что происходит с сессиями
Смена пароля отзываются все сессии пользователя, включая вашу собственную, если вы меняли пароль себе
Изменение роли, организации или переход в disabled отзываются все сессии пользователя
Сохранение грантов (PUT …/grants) отзываются все сессии получателя
Удаление пользователя сессии удаляются, а их идентификаторы заносятся в список отзыва, чтобы ноды перестали отдавать видео по уже выданным токенам
Вход при заданном max_sessions_per_user самые старые сессии сверх лимита отзываются: побеждает новейшая. Значение меньше 1 считается «без ограничения» и никого не отзывает

Клиент отличает эти случаи по ответу продления сессии: 401 {"error":"evicted"} — «вас вытеснили или отозвали», 401 {"error":"unauthorized"} — «токен просто негоден». Продление сессии заново считывает роль и гранты, поэтому изменение прав доходит до нод не позже следующего продления.

GET /api/v1/sessions — это не про входы

Метод с таким именем показывает зрителей живого видео по всему флоту, а не сессии входа в интерфейс. Он описан в разделе Ноды и хранилища.

Маршруты, которых нет#

Запрос Ответ Чем пользоваться вместо
GET /api/v1/tags/{id} 405 GET /api/v1/tags?q=<имя>
GET /api/v1/orgs/{id}/dvr-purge 405 выборка отправляется методом POST — и только после предпросмотра
любой неизвестный путь 501 {"error":"not_implemented"} проверьте написание пути: несуществующий путь не даёт 404