Организации и пользователи
Организации и квоты, пользователи и роли, гранты доступа, смена пароля, эффективные права.
Раздел описывает управление арендаторами и доступом: организации с их квотами, учётные записи, роли, гранты (правила доступа к камерам) и теги, по которым эти гранты выдаются.
Во всех примерах используются переменные:
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>
Общие соглашения — заголовки, коды ошибок, пагинация, вход и обновление сессии — в Введении и аутентификации. Те же операции через интерфейс описаны в разделах Организации, квоты и лимиты и Пользователи и права доступа.
- Заголовок
Content-Type: application/jsonобязателен при любом теле запроса — без него запрос отклоняется до обработчика с кодом415и текстовым (не JSON) телом. - Пустое значение числового параметра — ошибка, а не «параметр не задан»:
?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 | null → 90 |
глубина хранения событий, не меньше 1 |
placement_mode |
string | null | null → manual |
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 |