Учёт 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
This commit is contained in:
zuevav
2026-10-06 15:46:25 +03:00
parent fbf6a23667
commit a9915bcc82
8 changed files with 395 additions and 53 deletions
+125
View File
@@ -0,0 +1,125 @@
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
}