Files
Bridge-and-Join-s/internal/lkgateway/units.go
T
zuevav 3bb5e79272 Безопасность: остаток аудита — L2b, L3, L6, L7, L8, L10, L11, M4
- L2b: CSRF для изменяющих запросов админки — проверка Origin (иначе Referer)
  против Host на всех не-GET под /admin (sameOriginRequest в authMiddleware);
  за loopback-прокси учитывается X-Forwarded-Host. Второй слой поверх
  SameSite=Lax, без правки десятков форм настроек.
- L3: /back-to-admin меняет состояние — только POST + csrfOK (форма уже с токеном,
  проверки в обработчике не было).
- L6: EXEC процедуры DEPO по имени из настроек — через execProc с проверкой
  идентификатора (sqlident.go, см. коммит строчных событий).
- L7: сессия просмотра (impersonate) снимает IsAdmin цели — сотрудник не действует
  от имени другого администратора; свои действия — из своей сессии.
- L8: bootstrap администратора кабинета под замком lkBootstrapMu — два
  параллельных запроса не заведут двух админов (TOCTOU).
- L10: allowlist перезапуска служб не пропускает пустое имя.
- L11: ретранслятор — лимит тела проверяется по Content-Length до чтения и
  чтение ограничено RELAY_MAX_BODY+1.
- M4: .htaccess дополнительно запрещает *.lock (код установки), если данные
  оказались в веб-корне; nginx-запрет /data уже в README.

L4 (привязка сессии к сети по умолчанию выкл.) и L9 (косметическая cookie
bj_lk_admin, арбитр — ядро) оставлены как есть, отмечены в отчёте.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
2026-09-22 16:28:14 +03:00

128 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// units.go — состояние служб bj и её модулей для страницы настроек.
//
// Модули вынесены в отдельные службы (bj-npr, bj-quikexport, bj-lk), и когда одна
// из них лежит, раздел модуля в bj отвечает ошибкой связи. Раньше администратору
// оставалось идти в консоль за systemctl status; здесь он видит состояние всех
// служб и может перезапустить нужную. Читаем состояние обычным systemctl show
// (прав не требует), перезапуск делает root-демон bj-keymedia по закрытому списку.
import (
"context"
"net/http"
"os/exec"
"strings"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
)
// UnitView — служба для карточки настроек.
type UnitView struct {
Unit string // bj-npr
Title string // Риск-монитор НПР / УДС
Core bool // ядро (перезапуск рвёт текущую страницу)
State string // active | inactive | failed | unknown
Sub string // running, dead, …
Since string // с какого времени в этом состоянии
Result string // причина отказа (systemd Result)
Present bool // компонент установлен на этом сервере
}
// OK — служба работает.
func (u UnitView) OK() bool { return u.State == "active" && u.Sub == "running" }
// unitState — состояние одной службы (без прав root).
func unitState(unit string) UnitView {
v := UnitView{Unit: unit, State: "unknown"}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "systemctl", "show", unit,
"--property=ActiveState,SubState,ActiveEnterTimestamp,Result").Output()
if err != nil {
return v
}
for _, line := range strings.Split(string(out), "\n") {
k, val, ok := strings.Cut(strings.TrimSpace(line), "=")
if !ok || val == "" {
continue
}
switch k {
case "ActiveState":
v.State = val
case "SubState":
v.Sub = val
case "Result":
if val != "success" {
v.Result = val
}
case "ActiveEnterTimestamp":
// «Wed 2026-09-16 01:13:56 MSK» — оставляем дату и время
if f := strings.Fields(val); len(f) >= 3 {
v.Since = f[1] + " " + f[2]
}
}
}
return v
}
// unitViews — службы bj на этом сервере: ядро и установленные модули.
func unitViews(rc *RuntimeConfig) []UnitView {
installed := release.LoadInstalled(release.InstalledFile)
var out []UnitView
for _, c := range release.Components {
if c.Unit == "" {
continue
}
_, present := installed[c.Name]
if !present && !c.Core {
continue // модуля на сервере нет — и службы быть не должно
}
v := unitState(c.Unit)
v.Title, v.Core, v.Present = c.Title, c.Core, true
out = append(out, v)
}
return out
}
// restartUnit — перезапуск службы через root-демон (см. bj-keymedia-worker).
func restartUnit(ctx context.Context, unit string) (string, error) {
cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
res, err := runKeymediaOp(cctx, "unit-restart", map[string]string{"extra": unit})
if err != nil {
return "", err
}
if s, ok := res["output"].(string); ok && s != "" {
return s, nil
}
return "перезапущено: " + unit, nil
}
// unitRestart — POST /admin/setup/units/restart: перезапуск службы из настроек.
func (h *setupHandlers) unitRestart(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
unit := strings.TrimSpace(r.FormValue("unit"))
known := false
for _, c := range release.Components {
// пустой Unit у bj-component-install — не служба; пустая строка не должна проходить
if c.Unit != "" && c.Unit == unit {
known = true
}
}
if !known {
setupFlash(w, r, "Неизвестная служба.")
return
}
msg, err := restartUnit(r.Context(), unit)
if err != nil {
setupFlash(w, r, "Перезапуск "+unit+": "+err.Error())
return
}
setupFlash(w, r, msg)
}