Кабинет клиента: документы и корпоративные действия, дистрибутив, обратный канал, защита
Данные - корпоративные действия по бумагам клиента (U_CA_ACTS), готовые выписки (U_OP_INF_EXTRACT), заявки клиента на документ (lk.doc_requests) с оповещением сотрудникам; страница «Документы» - дистрибутив кабинета из настроек ядра: архив с программой, службой, настройками, установщиком и инструкцией; ключ доступа на странице показан замаскированным Связь без публичного адреса у ядра - режим ожидания (BJ_MODE=agent): кабинет копит запросы, ядро само подключается к нему, выполняет их обработчиком /api/lk/* и возвращает ответы; 4 параллельных подключения, тот же ключ модуля в обе стороны, состояние канала в настройках - сквозной тест: настоящий кабинет + настоящий цикл ядра Защита (по итогам аудита) - вход: лимит попыток по адресу и логину, одинаковое время ответа для несуществующего логина, пароль от 10 символов - сессии ядра: на диске отпечаток токена и ссылка на учётную запись, без персональных данных; карточка клиента читается из БД на каждый запрос (блокировка действует сразу); абсолютный срок 24 ч; привязка к браузеру, к сети — по настройке - вход сотрудника в кабинет клиента пишется в lk.audit и поднимает оповещение - кабинет: CSRF на всех формах, /impersonate только POST, куки HttpOnly+Secure+SameSite=Strict, выход гасит все сессии, заголовки CSP/X-Frame-Options/nosniff/HSTS, no-store, таймауты сервера, ограничение тела, recover; сообщения через куку вместо адреса; /health наружу без подробностей - шрифты вшиты в программу: страница не обращается к сторонним сайтам - экранирование LIKE в выборках по номеру договора; ключ и токен без суррогата при отказе rand - исправлено: ошибка входа не показывалась клиенту (шаблон ждал Error, приходил Flash) Тесты: lksecurity_test, lkagent_test, lkdist_test, modules/lk (CSRF, заголовки, режим ожидания), живые lkcoverage/lkunpriced, scripts/lk-sec-smoke.sh — проверка работающего кабинета снаружи. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package lkgateway
|
||||
|
||||
// lkaudit.go — журнал входов сотрудника в кабинет клиента.
|
||||
//
|
||||
// Сотрудник может открыть кабинет клиента, чтобы помочь ему или подготовить поручение.
|
||||
// Это доступ к персональным данным, поэтому каждый такой вход записывается: кто, к кому,
|
||||
// когда и с какого адреса. Запись нужна и для внутреннего контроля, и чтобы ответить на
|
||||
// вопрос клиента «кто смотрел мои данные».
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// lkAuditSchema — журнал доступа (создаётся вместе со схемой lk).
|
||||
const lkAuditSchema = `
|
||||
CREATE TABLE IF NOT EXISTS lk.audit (
|
||||
id bigserial PRIMARY KEY,
|
||||
at timestamptz NOT NULL DEFAULT now(),
|
||||
event text NOT NULL,
|
||||
actor text NOT NULL,
|
||||
target_login text,
|
||||
target_contract text,
|
||||
ip text
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_lk_audit_at ON lk.audit (at DESC);
|
||||
CREATE INDEX IF NOT EXISTS ix_lk_audit_target ON lk.audit (target_contract, at DESC);
|
||||
`
|
||||
|
||||
// lkAuditImpersonate — записать вход сотрудника в кабинет клиента.
|
||||
func lkAuditImpersonate(ctx context.Context, rc *RuntimeConfig, actor, targetLogin, contract, ip string) {
|
||||
go func() {
|
||||
bg, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
db, err := lkDB(rc)
|
||||
if err != nil {
|
||||
log.Printf("кабинет: журнал доступа недоступен: %v", err)
|
||||
return
|
||||
}
|
||||
if _, err := db.ExecContext(bg, `INSERT INTO lk.audit (event, actor, target_login, target_contract, ip)
|
||||
VALUES ('impersonate', $1, $2, $3, $4)`, actor, targetLogin, contract, ip); err != nil {
|
||||
log.Printf("кабинет: не записан вход в кабинет клиента: %v", err)
|
||||
}
|
||||
}()
|
||||
raiseSystemAlert(rc, "Кабинет: вход сотрудника в кабинет клиента",
|
||||
fmt.Sprintf("Сотрудник %s открыл кабинет клиента %s (договор %s) с адреса %s. Доступ только на просмотр, запись в журнале lk.audit.",
|
||||
actor, targetLogin, contract, ip))
|
||||
}
|
||||
|
||||
// lkAuditRecord — строка журнала для страницы настроек.
|
||||
type lkAuditRecord struct {
|
||||
At time.Time `json:"at"`
|
||||
Event string `json:"event"`
|
||||
Actor string `json:"actor"`
|
||||
Target string `json:"target"`
|
||||
Contract string `json:"contract"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// lkAuditList — последние записи журнала доступа.
|
||||
func lkAuditList(ctx context.Context, rc *RuntimeConfig, limit int) ([]lkAuditRecord, error) {
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 100
|
||||
}
|
||||
db, err := lkDB(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := db.QueryContext(ctx, `SELECT at, event, actor, COALESCE(target_login,''), COALESCE(target_contract,''), COALESCE(ip,'')
|
||||
FROM lk.audit ORDER BY at DESC LIMIT $1`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []lkAuditRecord
|
||||
for rows.Next() {
|
||||
var a lkAuditRecord
|
||||
if rows.Scan(&a.At, &a.Event, &a.Actor, &a.Target, &a.Contract, &a.IP) == nil {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user