Skip to content

feat(modules): log who started a module operation - #1099

Merged
jorikfon merged 1 commit into
developfrom
feat/module-toggle-audit-log
Aug 5, 2026
Merged

feat(modules): log who started a module operation#1099
jorikfon merged 1 commit into
developfrom
feat/module-toggle-audit-log

Conversation

@jorikfon

@jorikfon jorikfon commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Включение и выключение модуля через REST не оставляло следа в system/messages — «кто выключил модуль» приходилось восстанавливать по nginx/access.log. Добавлена одна строка аудита на операцию.

Как устроено

ModulesManagementProcessor::callBack() читает $request['sessionContext'] (штатный конверт, документирован в PBXCoreREST/CLAUDE.md) и прокидывает его в startModuleOperation(). Строка пишется после успешного claim в журнале, поэтому отклонённые (409) попытки не выглядят как выполненные операции.

Пример:

Module operation started: {"operation":"enable","module":"ModuleCdrTags","user":"admin","ip":"127.0.0.1","operationId":"2474061275045daa94e643f9"}
  • Инициатор: JWT-вызов — имя пользователя; вызов по API-ключу — api + tokenId; localhost и внутренние вызовы — system.
  • reason логируется для disable, чтобы отличать DISABLED_BY_USER от DISABLED_BY_LICENSE и срабатывания crash-loop-вотчдога. reasonText намеренно не логируется — туда попадает текст исключения.
  • Батч-обновление: контекст сохраняется в состоянии батча, поэтому каждый модуль, поставленный в очередь UpdateAllModulesAction, сохраняет атрибуцию администратора, а не пишется как system.
  • JSON-кодирование контекста — защита от лог-инъекции через имя пользователя, паттерн взят из WorkerCallEvents/DeleteCDR.php.
  • LOG_WARNING, а не LOG_NOTICE: порог логгера — core.logsLevel (по умолчанию 4), строка уровня NOTICE в system/messages не попадает вовсе (проверено эмпирически).

Побочный эффект: ту же строку получают install и uninstall — они идут через ту же точку входа. Это улучшение, но упоминаю явно, чтобы не выглядело расширением области.

Проверено на стенде 172.16.32.85

  • localhost: {"operation":"disable","module":"ModuleCdrTags","user":"system","ip":"local","reason":"DISABLED_BY_USER",...}
  • с JWT: {"operation":"enable","module":"ModuleCdrTags","user":"admin","ip":"127.0.0.1",...}
  • updateAll вызван с новой сигнатурой (без реального обновления модулей) — TypeError нет; сам enqueue батча end-to-end не прогонялся, чтобы не тянуть пакеты на стенд.

Что осталось непокрытым (осознанно)

  • Ветки useLegacyInstallPipeline() (аварийный переключатель на один релиз) минуют startModuleOperation() и ничего не пишут. У enable/disable легаси-ветки нет.
  • Автоматическое отключение через PbxExtensionUtils::forceDisableModule() (несовместимая версия, битый module.json) логируется только при неудаче — это отдельный след, вне области REST-аудита.
  • Crash-loop-вотчдог уже пишет свою строку в WorkerSafeScriptsCore перед отключением, дублировать не стал.

Enabling or disabling a module over REST left no trace in system/messages, so
answering "who turned this module off" meant digging through nginx/access.log.

ModulesManagementProcessor now forwards the REST sessionContext into
startModuleOperation() and writes one audit line right after the journal claim
succeeds — rejected (409) attempts are not recorded as operations that ran. The
line covers install and uninstall as well, since they share the same entry
point. Context is JSON-encoded to keep crafted user names from forging fields,
matching the pattern in WorkerCallEvents/DeleteCDR.

Initiator resolution: JWT callers log their user name, raw API-Key callers log
`api` plus the token id, localhost and internal calls log `system`. For a batch
update the context is stored in the batch state, so every module enqueued by
UpdateAllModulesAction keeps the attribution of the admin who started it.

Uses LOG_WARNING deliberately: the logger threshold is core.logsLevel (4 by
default) and a LOG_NOTICE line never reaches system/messages.
@jorikfon

jorikfon commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Уточнение по проверкам.

Проверено на стенде 172.16.32.85 end-to-end: одиночные enable/disable через REST пишут строку с корректным инициатором (user=admin, ip=127.0.0.1 для JWT; user=system, ip=local для localhost).

Батч-путь (updateAll → состояние батча → enqueueInstallFromRepo) на стенде подтвердить не удалось: очередь api:requests разбирается тремя живыми WorkerApiCommands мгновенно, поэтому прочитать поставленное сообщение из CLI не получается, а гонять реальное обновление модуля на стенде я не стал. Путь проверен по коду: saveBatchState() сериализует состояние целиком через json_encode($state, JSON_UNESCAPED_SLASHES), так что вложенный sessionContext переживает Redis, а startNextModule() читает его обратно и передаёт в enqueueInstallFromRepo(), который кладёт ключ в конверт очереди. Если при ревью есть возможность прогнать реальное «Обновить все» — это единственное место, которое стоит посмотреть глазами.

@jorikfon
jorikfon marked this pull request as ready for review August 5, 2026 08:25
@jorikfon
jorikfon merged commit 617ca3a into develop Aug 5, 2026
1 check passed
@jorikfon
jorikfon deleted the feat/module-toggle-audit-log branch August 5, 2026 08:25
@jorikfon

jorikfon commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Батч-путь подтверждён на стенде 172.16.32.85 — «Обновить все» из веб-интерфейса:

Module operation started: {"operation":"install_repo","module":"ModuleCTIClient","user":"admin","ip":"172.16.33.40","operationId":"e7e6b53cb140abe098c7c84b"}

user/ip — реальный инициатор, а не system/local, то есть контекст пережил сериализацию состояния батча в Redis и доехал до сообщения в очереди. Оговорка из предыдущего комментария снимается: непроверенных путей не осталось.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant