50e420f4a0
Мастер, шаг «Учёт DEPO», и «Обмен с DEPO» в настройках: СУБД «Firebird» (Fancy на Firebird 2.5). - Подключение: драйвер nakagami/firebirdsql, путь к файлу базы или псевдоним, WIN1251. Запросы bj к DEPO (T-SQL) подключение переводит само (fancyfb.go): механически (dbo., TOP, ISNULL, GETDATE, LEN, N, exec) вне строк и комментариев, точные тексты — fsql/fb.go (каталоги, LIKE [_], COUNT(*) OVER, SELECT без FROM, PATINDEX); @имена — позиционные ?, GUID — в верхнем регистре. Системные запросы из кода — в fsql/depocatalog.go. - «Подготовить» логином SYSDBA (depo_setup_fb.go): проверка базы и UDF Fancy, логин SYSDBA_BJ (Fancy даёт права на документы логинам SYSDBA%; для Firebird — обычный пользователь), оператор-робот Fancy, кодировщик M2M, привязки FTV_M2M_SETTINGS по признакам Fancy, таблицы, процедуры, права; обработку запускает bj. «Получить скрипт» — то же для isql в WIN1251; письмо администратору, установка процедур, плашка обновления. - Скрипты Firebird встроены (depoprocs/firebird, тест сверяет с deploy/); количества — DOUBLE PRECISION: из диалекта 3 в базу диалекта 1 процедуру с NUMERIC(18,7) не создать. - Проверено на копии базы Fancy (отдельный экземпляр bj): мастер с нуля — все проверки, BJ_PING; тестовый режим DEPO — К1 Бубликов, зачисление и списание до исполнения без ошибок; очередь, справочник НРД. Тест FBLive готовит на базе все запросы bj к DEPO: 94 из 101, оставшиеся 7 — ЛК и ручной ввод (следующий этап). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eREaYwUt1i4fNLZcGnjn
129 lines
5.6 KiB
Go
129 lines
5.6 KiB
Go
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 fbIsLoginFailure(err)
|
||
}
|
||
|
||
// 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
|
||
}
|
||
if s.IsFirebird() { // запросы bj (T-SQL) — в диалект Firebird
|
||
base = fbConnector{base: base}
|
||
}
|
||
return sql.OpenDB(&fancyConnector{base: base, key: depoLoginKey(s), user: s.User, srv: s.Server}), nil
|
||
}
|