Кабинет клиента: документы и корпоративные действия, дистрибутив, обратный канал, защита
Данные - корпоративные действия по бумагам клиента (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:
@@ -45,8 +45,8 @@ func lkCreateUser(ctx context.Context, rc *RuntimeConfig, u LKUser, password str
|
||||
if strings.TrimSpace(u.Login) == "" || strings.TrimSpace(u.Contract) == "" {
|
||||
return errors.New("логин и номер договора обязательны")
|
||||
}
|
||||
if len(password) < 6 {
|
||||
return errors.New("пароль минимум 6 символов")
|
||||
if err := lkCheckPassword(password); err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -68,8 +68,8 @@ func lkCreateUser(ctx context.Context, rc *RuntimeConfig, u LKUser, password str
|
||||
|
||||
// lkSetPassword меняет пароль пользователя ЛК.
|
||||
func lkSetPassword(ctx context.Context, rc *RuntimeConfig, login, password string) error {
|
||||
if len(password) < 6 {
|
||||
return errors.New("пароль минимум 6 символов")
|
||||
if err := lkCheckPassword(password); err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
@@ -109,6 +109,9 @@ func lkAuth(ctx context.Context, rc *RuntimeConfig, login, password string) (*LK
|
||||
&u.FullName, &u.Email, &u.Phone, &u.Active,
|
||||
&u.LastName, &u.FirstName, &u.MiddleName,
|
||||
&u.DocType, &u.DocSeries, &u.DocNumber, &u.IsAdmin, &u.CanTransfer); err != nil {
|
||||
// Логина нет. Всё равно считаем bcrypt по фиктивному хешу: иначе ответ
|
||||
// приходит заметно быстрее и по времени можно перебрать существующие логины.
|
||||
_ = bcrypt.CompareHashAndPassword([]byte(lkDummyHash), []byte(password))
|
||||
return nil, errLKAuth // не раскрываем, что логина нет
|
||||
}
|
||||
if !u.Active {
|
||||
@@ -146,3 +149,51 @@ func lkListUsers(ctx context.Context, rc *RuntimeConfig) ([]LKUser, error) {
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// lkDummyHash — заведомо не подходящий bcrypt-хеш: по нему считаем проверку, когда
|
||||
// логина нет, чтобы ответ занимал столько же времени, сколько при существующем логине.
|
||||
const lkDummyHash = "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
|
||||
|
||||
// lkCheckPassword — требования к паролю кабинета: в кабинете видны паспортные данные
|
||||
// и остатки на счетах депо, поэтому шести символов мало.
|
||||
func lkCheckPassword(password string) error {
|
||||
if len([]rune(password)) < 10 {
|
||||
return errors.New("пароль минимум 10 символов")
|
||||
}
|
||||
var hasLetter, hasOther bool
|
||||
for _, r := range password {
|
||||
switch {
|
||||
case (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= 'а' && r <= 'я') || (r >= 'А' && r <= 'Я'):
|
||||
hasLetter = true
|
||||
default:
|
||||
hasOther = true
|
||||
}
|
||||
}
|
||||
if !hasLetter || !hasOther {
|
||||
return errors.New("пароль должен содержать буквы и хотя бы одну цифру или знак")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lkUserByID — карточка клиента по идентификатору. Нужна сессиям: на диске хранится
|
||||
// только ссылка на учётную запись, а сами данные читаются отсюда на каждый запрос.
|
||||
func lkUserByID(ctx context.Context, rc *RuntimeConfig, id int64) (*LKUser, error) {
|
||||
db, err := lkDB(rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
var u LKUser
|
||||
if err := db.QueryRowContext(cctx, `SELECT id, login, contract_number,
|
||||
COALESCE(full_name,''), COALESCE(email,''), COALESCE(phone,''), active,
|
||||
COALESCE(last_name,''), COALESCE(first_name,''), COALESCE(middle_name,''),
|
||||
COALESCE(doc_type,''), COALESCE(doc_series,''), COALESCE(doc_number,''), is_admin, can_transfer
|
||||
FROM lk.users WHERE id=$1`, id).Scan(&u.ID, &u.Login, &u.Contract,
|
||||
&u.FullName, &u.Email, &u.Phone, &u.Active,
|
||||
&u.LastName, &u.FirstName, &u.MiddleName,
|
||||
&u.DocType, &u.DocSeries, &u.DocNumber, &u.IsAdmin, &u.CanTransfer); err != nil {
|
||||
return nil, errors.New("учётная запись не найдена")
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user