Согласование изменений
Владелец рабочего пространства может потребовать, чтобы административные изменения подтверждали участники, прежде чем они применятся. Это отдельный механизм от аппрувов транзакций: правило задаётся на действие, а не политикой, и подтверждают его владелец, администраторы или выбранная группа.
По умолчанию согласование выключено. Владелец включает его для каждого действия отдельно.
Какие действия
| Действие | Что покрывает | operation в запросе |
|---|---|---|
| Управление пользователями | приглашение пользователя, создание API-пользователя, удаление пользователя и API-пользователя, смена роли | create, api_user_create, delete, role_change |
| Управление группами | создание, переименование и удаление группы, состав участников | create, update, delete, add_members, remove_members |
| Управление согласованием | порог администраторов и правила согласования, включая их выключение | threshold, action_setting |
| Добавление адреса в белый список | новый адрес во внешнем кошельке | нет |
| Управление тегами | создание, изменение и удаление тега, привязка и отвязка тегов к аккаунтам | create, update, delete, attach_detach |
| Переводы на one-time адреса | разрешение и запрет переводов по адресу вне белого списка | нет |
| Управление политиками | изменение правил политики и откат к версии из истории | update, rollback |
Правило одно на действие: нельзя требовать согласования удаления, но не
смены роли. Изменения администратора платформы (PLATFORM_ADMIN)
согласования не требуют; у поддержки платформы (PLATFORM_SUPPORT) такого
обхода нет.
Кто подтверждает
| Режим | Значение mode |
Кто подтверждает |
|---|---|---|
| Владелец | QUORUM_APPROVAL_MODE_OWNER_ONLY |
владелец рабочего пространства |
| Кворум администраторов | QUORUM_APPROVAL_MODE_ADMIN_QUORUM |
администраторы, включая неподписывающих; порог общий для всех действий в этом режиме |
| Группа | QUORUM_APPROVAL_MODE_SPECIFIC_GROUP |
участники выбранной группы, чьей роли доступно мобильное приложение (все роли, кроме наблюдателя и оператора), со своим порогом |
К кворуму администраторов или группе можно добавить обязательное подтверждение владельца.
Режим «Группа» требует включённого согласования действия «Управление группами»; само это действие в режиме «Группа» включается.
| Изменение правил | Ответ |
|---|---|
| включить правило в режиме «Группа» или перевести на группу включённое, пока согласование действия «Управление группами» выключено | QUORUM_GROUP_MANAGEMENT_APPROVAL_DISABLED |
| выключить согласование действия «Управление группами», пока включено правило в режиме «Группа» | QUORUM_GROUP_MANAGEMENT_APPROVAL_IN_USE; в details.action_keys — ключи таких правил |
Подтверждение принимается только с привязанного устройства в мобильном приложении — это второй фактор. Инициатор тоже подтверждает свой запрос, владелец в режиме «Владелец» подтверждает сам.
Заблокировать рабочее пространство нельзя
Правила согласования подтверждает только владелец. Правило, состав
которого сейчас не набирает порог, включить нельзя: ответ
QUORUM_NO_ELIGIBLE_APPROVERS.
Порог администраторов не поднимается выше их числа, пока включено
правило в режиме кворума.
Настройки согласования меняет владелец. Администраторы, включая неподписывающих, могут предлагать изменения, пока владелец держит включённым согласование действия «Управление согласованием». Выключение этого согласования отменяет неподтверждённые запросы на изменение настроек.
| «Управление согласованием» | Изменение владельца | Изменение администратора |
|---|---|---|
| выключено (по умолчанию) | применяется сразу | не принимается: QUORUM_CHANGE_REQUESTS_DISABLED |
| включено | запрос на согласование, владелец подтверждает сам | запрос на согласование, подтверждает владелец |
Как проходит согласование
- Инициатор выполняет действие как обычно. Если для него включено
согласование, изменение не применяется, а ответ приходит с кодом
QUORUM_APPROVAL_REQUIREDи идентификатором запроса. - Тем, кто подтверждает, приходит push-уведомление на привязанное устройство. Они подтверждают или отклоняют запрос в мобильном приложении.
- Когда подтверждений достаточно, изменение проверяется заново и применяется само. Повторять действие не нужно.
- Отказ, после которого порог уже не набрать, закрывает запрос. Пока запрос ждёт голосов, инициатор может его отменить. Запрос без нужных голосов истекает через 72 часа.
Права инициатора при применении перепроверяются у изменений пользователей, групп и настроек согласования. Адрес белого списка, тег, переводы на one-time адреса и политика применяются и тогда, когда инициатор к этому моменту потерял права.
Повторная попытка того же изменения, пока запрос ждёт голосов, новый запрос не создаёт.
| Статус | Значение |
|---|---|
PENDING |
ждёт подтверждений |
APPROVED |
подтверждений достаточно, изменение применяется |
EXECUTED |
изменение применено. Если применять к этому моменту уже нечего, правка тега, привязка уже привязанного тега и настройка one-time адресов тоже закрываются EXECUTED — без изменений |
FAILED |
подтверждено, но применить нельзя; причина — в карточке запроса. Например: пользователь уже в этой роли или уже в группе, адрес уже в белом списке, тег уже отвязан, политика изменилась после запроса |
REJECTED |
отклонено |
EXPIRED |
72 часа прошли без нужных подтверждений |
CANCELLED |
отменено до решения |
Ход согласования приходит событиями по WebSocket участникам согласования: инициатору, тем, кто подтверждает, и тем, кто видит все запросы рабочего пространства. На привязанное устройство события приходят только инициатору и тем, кто подтверждает. Каждое решение попадает в журнал аудита.
Для интеграции
Ответ на действие, которому нужно согласование:
{
"error": "change requires quorum approval",
"code": "QUORUM_APPROVAL_REQUIRED",
"details": {
"approval_request_uuid": "3f0c5c1e-…",
"action": "<action_key>",
"operation": "delete",
"mode": "QUORUM_APPROVAL_MODE_OWNER_ONLY",
"expires_at": "2026-09-17T10:00:00Z"
}
}
Ключ действия отдаёт GET …/quorum/actions полем key, в ответе на
действие он приходит полем action. Режим везде передаётся значением из
таблицы режимов: в ответе на действие, в запросах согласования и в
настройках.
| Ответ действия | Когда |
|---|---|
403 QUORUM_APPROVAL_REQUIRED |
правило включено; запрос зарегистрирован, его данные — в details |
409 |
правило включено, но подтверждать сейчас некому; изменение не сохраняется. У управления пользователями, группами и согласованием код ошибки — QUORUM_NO_ELIGIBLE_APPROVERS |
503 CHANGE_APPROVAL_UNAVAILABLE |
согласование временно недоступно; изменение не сохраняется, повторите позже |
| Метод | Путь | Право |
|---|---|---|
GET |
/api/v2/admin/tenants/{tenant_uuid}/change-approvals?scope=PENDING_MY_VOTE\|INITIATED_BY_ME\|ALL |
пользователь рабочего пространства; ALL — VIEW_CHANGE_APPROVALS; с привязанного устройства — только PENDING_MY_VOTE и INITIATED_BY_ME пользователя, привязавшего устройство к этому рабочему пространству |
GET |
/api/v2/admin/change-approvals?scope=… — тот же список сразу по всем рабочим пространствам, доступным токену; страница общая |
как у списка рабочего пространства, по каждому пространству отдельно; ALL — только пространства с VIEW_CHANGE_APPROVALS, ни одного — 403 |
GET |
…/change-approvals/{request_uuid} |
участник запроса или VIEW_CHANGE_APPROVALS; с устройства — участник |
POST |
…/change-approvals/{request_uuid}/approve, …/reject |
участник согласования, с привязанного устройства |
POST |
…/change-approvals/{request_uuid}/cancel |
инициатор запроса; с привязанного устройства — если роль инициатора требует привязки устройства |
GET |
/api/v2/admin/tenants/{tenant_uuid}/quorum |
VIEW_QUORUM_SETTINGS |
GET |
…/quorum/actions — список действий: ключ key (он же {action_key} в пути PUT) и fixed_mode у действия, режим которого не выбирается |
VIEW_QUORUM_SETTINGS |
PUT |
…/quorum/actions/{action_key}, …/quorum/threshold |
MANAGE_QUORUM_SETTINGS; PROPOSE_QUORUM_SETTINGS — только запросом, пока включено «Управление согласованием» |
GET |
…/quorum/gated-actions — действия рабочего пространства и requires_approval: потребует ли действие у вызывающего согласования; предупреждение до отправки изменения, прав на настройки не требует |
пользователь или API-пользователь с активной ролью в рабочем пространстве; с устройства — 403 |
Устройству, привязанному в нескольких рабочих пространствах, хватает одного
запроса: у каждого элемента списка своё пространство в tenant_uuid и
tenant_name, а initiated_by_me считается по пользователю, привязавшему
устройство к этому пространству. Карточка и голос — по путям пространства
элемента.
GET /api/v2/admin/change-approvals?scope=PENDING_MY_VOTE&limit=50
{
"items": [
{ "request_uuid": "3f0c5c1e-…", "tenant_uuid": "0f8b7a5e-…", "tenant_name": "Treasury",
"action": "<action_key>", "action_display_name": "Manage users", "status": "PENDING", "initiated_by_me": false },
{ "request_uuid": "a91d07b2-…", "tenant_uuid": "7c2e41d9-…", "tenant_name": "Trading",
"action": "<action_key>", "action_display_name": "Manage tags", "status": "PENDING", "initiated_by_me": true }
],
"total": 2,
"limit": 50,
"offset": 0
}
Ответ на принятый голос и на отмену — обновлённая карточка запроса. Отмена
переводит в CANCELLED запрос, открытые sets[] и неподанные голоса,
поданные голоса остаются.
POST /api/v2/admin/tenants/{tenant_uuid}/change-approvals/3f0c5c1e-…/cancel
{
"request": {
"request_uuid": "3f0c5c1e-…",
"status": "CANCELLED",
"sets": [
{ "external_id": "OWNER", "threshold": 1, "status": "CANCELLED",
"approvers": [ { "user_uuid": "5b2f…", "status": "CANCELLED", "is_me": false } ] }
]
}
}
| Ответ | На что | Когда |
|---|---|---|
CHANGE_VOTE_DEVICE_REQUIRED |
approve, reject |
голос не с привязанного устройства |
CHANGE_VOTE_NOT_ALLOWED |
approve, reject |
голосующий не входит в состав подтверждающих запроса |
CHANGE_VOTE_CONFLICT |
approve, reject |
противоположный голос уже подан; повтор того же голоса ничего не меняет |
CHANGE_REQUEST_CANCEL_NOT_ALLOWED |
cancel |
отменяет не инициатор запроса |
200, та же карточка |
cancel |
запрос уже отменён |
CHANGE_REQUEST_ALREADY_FINALIZED |
все три | запрос уже закрыт или истёк |
CHANGE_REQUEST_NOT_FOUND |
все три | запроса нет в рабочем пространстве; то же у карточки |
Карточка запроса несёт всё, что нужно показать человеку, без дополнительных запросов:
{
"request": {
"request_uuid": "3f0c5c1e-…",
"tenant_uuid": "…",
"tenant_name": "Treasury",
"action": "<action_key>",
"action_display_name": "Manage users",
"initiator": { "user_uuid": "7d1e…", "user_type": "user", "email": "owner@example.com", "first_name": "Anna", "last_name": "Smirnova", "api_user_name": "" },
"initiated_by_me": false,
"payload": { "operation": "delete", "tenant_uuid": "…", "user_uuid": "9a4c…" },
"display": { "user": { "user_uuid": "9a4c…", "user_type": "user", "email": "ivan@example.com", "first_name": "Ivan" }, "current_role": "EDITOR" },
"status": "PENDING",
"sets": [
{
"external_id": "OWNER",
"threshold": 1,
"status": "PENDING",
"approvers": [ { "user_uuid": "5b2f…", "status": "PENDING", "user": { "user_uuid": "5b2f…", "user_type": "user", "email": "boss@example.com", "first_name": "", "last_name": "", "api_user_name": "" }, "is_me": true } ]
}
]
}
}
| Поле | Что содержит |
|---|---|
initiator, sets[].approvers[].user |
участник: user_type — user или api_user; у пользователя email, first_name, last_name, у API-пользователя api_user_name |
initiated_by_me, sets[].approvers[].is_me |
этот участник — вы; с привязанного устройства — пользователь, привязавший его к этому рабочему пространству |
payload |
что будет изменено |
display |
названия и имена того, на что ссылается payload, на момент создания запроса; поля нет, если снимка нет |
| Действие | operation |
Поля display |
|---|---|---|
| Управление пользователями | role_change, delete |
user, current_role |
| Управление группами | update, delete |
group_name |
add_members, remove_members |
group_name, users — по порядку payload; у длинного списка только первые участники |
|
| Управление согласованием | action_setting |
action_display_name, group_name |
| Добавление адреса в белый список | нет | whitelisted_wallet_name, currency_symbol, currency_descriptor, blockchain_name |
| Управление тегами | update, delete |
tag |
attach_detach |
vaults, tags |
|
| Управление политиками | update |
policy_type_name |
rollback |
policy_type_name, version_published_at |
Остальные операции приходят без display: их payload читается как есть.
Состав подтверждающих в ответе настроек:
| Поле | Что содержит |
|---|---|
approver_sets, reachable у правила |
кто подтверждал бы сейчас и набирает ли состав порог; есть и у выключенных правил |
owner_approvers, admin_quorum_approvers |
кто прямо сейчас подтверждает как владелец и как администратор, независимо от правил |
active_member_uuids у группы в списке групп |
участники, которые считаются в режиме «Группа» |
Состав ещё не сохранённого правила считается из этих пулов, участников группы и порога.