Files
Bridge-and-Join-s/internal/lkgateway/fancygate.go
T
zuevav a9915bcc82 Учёт DEPO: ход подготовки по шагам, понятный блок «Логин bj», пауза после неудачного входа
- «Подготовить» / «Проверить подключение» показывают ход: подключение логином
  администратора, проверка, что это база DEPO, применение скрипта, вход логином bj,
  таблицы обмена, задание, процедуры, «Готово». Сервер отдаёт шаги строками JSON
  (stream=1), на ошибке — шаг с крестиком и причина, без перехода со страницы.
- Блоки «Администратор SQL — нужен один раз, не сохраняется» и «Логин bj — под ним
  bj будет работать»: bj сохраняет и использует ровно то, что вписано; «другой
  пароль»; на итоге шага — каким логином bj работает.
- Пауза 15 минут после неудачного входа в DEPO (логин/пароль отвергнуты) для
  фоновых служб: с неверным паролем они за минуты блокировали логин на сервере
  (CHECK_POLICY) у всех, кто им пользуется. Действия человека идут без паузы;
  новый пароль — сразу; системное событие в «Новостях».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-06 15:46:25 +03:00

126 lines
5.5 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
// Защита логина DEPO от блокировки (06.10.2026). На SQL Server логины с CHECK_POLICY подчиняются политике
// блокировки Windows: несколько неудачных входов подряд — и логин заблокирован для всех, кто им пользуется.
// Службы bj ходят в DEPO каждые несколько секунд; с неверным паролем (сменили на сервере, опечатка в
// настройке, второй bj с тем же логином) они за минуты блокировали логин — так 06.10 тестовая ВМ с логином
// bj_user остановила обмен боевого bj. Поэтому после неудачного входа bj с этими логином и паролем больше
// не пробует depoLoginPause — кроме входа, который человек запустил сам (проверка в мастере, кнопки
// настройки): для него depoLoginForce. Новый пароль — новая попытка сразу.
import (
"context"
"crypto/sha256"
"database/sql"
"database/sql/driver"
"encoding/hex"
"errors"
"fmt"
"strconv"
"sync"
"time"
"github.com/jackc/pgx/v5/pgconn"
mssql "github.com/microsoft/go-mssqldb"
)
// depoLoginPause — сколько не повторять вход после неудачи (политика блокировки считает попытки за окно;
// раз в 15 минут — заведомо реже порога).
const depoLoginPause = 15 * time.Minute
type depoLoginFail struct {
at time.Time
err string
}
var depoLoginFails = struct {
sync.Mutex
m map[string]depoLoginFail
}{m: map[string]depoLoginFail{}}
type depoLoginForceKey struct{}
// depoLoginForce — вход по действию человека: выполняется и во время паузы после неудачи.
func depoLoginForce(ctx context.Context) context.Context {
return context.WithValue(ctx, depoLoginForceKey{}, true)
}
// depoLoginKey — сервер, логин и отпечаток пароля (сам пароль в памяти ключом не держим).
func depoLoginKey(s FancyMSSQLSettings) string {
h := sha256.Sum256([]byte(s.Password))
return fancyDriver(s) + "|" + s.Server + "|" + strconv.Itoa(s.Port) + "|" + s.User + "|" + hex.EncodeToString(h[:8])
}
// depoIsLoginFailure — сервер отверг логин или пароль (то, что считает политика блокировки).
func depoIsLoginFailure(err error) bool {
var me mssql.Error
if errors.As(err, &me) {
switch me.Number {
case 18456, 18486, 18487, 18488: // неверный логин/пароль, заблокирован, пароль истёк, нужно сменить
return true
}
}
var pe *pgconn.PgError
if errors.As(err, &pe) {
return pe.Code == "28P01" || pe.Code == "28000"
}
return false
}
// fancyConnector — подключение к DEPO через паузу после неудачного входа.
type fancyConnector struct {
base driver.Connector
key string
user string
srv string
}
func (c *fancyConnector) Driver() driver.Driver { return c.base.Driver() }
func (c *fancyConnector) Connect(ctx context.Context) (driver.Conn, error) {
force, _ := ctx.Value(depoLoginForceKey{}).(bool)
depoLoginFails.Lock()
f, failed := depoLoginFails.m[c.key]
depoLoginFails.Unlock()
if failed && !force && time.Since(f.at) < depoLoginPause {
return nil, fmt.Errorf("вход в DEPO логином %s не удался в %s (%s) — bj не повторяет его до %s, чтобы неудачные попытки не заблокировали логин на сервере. Проверьте логин и пароль: Настройка → «Учёт DEPO»",
c.user, f.at.Format("15:04"), f.err, f.at.Add(depoLoginPause).Format("15:04"))
}
conn, err := c.base.Connect(ctx)
switch {
case err != nil && depoIsLoginFailure(err):
depoLoginFails.Lock()
depoLoginFails.m[c.key] = depoLoginFail{at: time.Now(), err: quikErrorDetail(err)}
depoLoginFails.Unlock()
if !failed && !force {
raiseSystemAlert(globalRC, "DEPO: вход логином "+c.user+" не удался",
"Сервер "+c.srv+" отверг логин или пароль bj. Чтобы неудачные попытки не заблокировали логин, bj не повторяет вход "+
strconv.Itoa(int(depoLoginPause/time.Minute))+" минут. Проверьте логин и пароль в настройке «Учёт DEPO».\n\n"+quikErrorDetail(err))
}
case err == nil && failed:
depoLoginFails.Lock()
delete(depoLoginFails.m, c.key)
depoLoginFails.Unlock()
}
return conn, err
}
// fancyOpenGated — пул подключений к DEPO с паузой после неудачного входа.
func fancyOpenGated(s FancyMSSQLSettings, dsn string) (*sql.DB, error) {
probe, err := sql.Open(fancyDriver(s), dsn) // без подключения: только чтобы взять драйвер
if err != nil {
return nil, err
}
drv := probe.Driver()
_ = probe.Close()
dc, ok := drv.(driver.DriverContext)
if !ok {
return sql.Open(fancyDriver(s), dsn)
}
base, err := dc.OpenConnector(dsn)
if err != nil {
return nil, err
}
return sql.OpenDB(&fancyConnector{base: base, key: depoLoginKey(s), user: s.User, srv: s.Server}), nil
}