cf094f6e6f
Проход по операциям, которые делают не то, что обещает их имя. P0. Удаление bootstrap-admin-peer не было отзывом доступа. Признаком «создавать пир или нет» служило наличие строки в таблице, а HY2XS_ADMIN_CON_PASS продолжает жить в /etc/hy2xs/hy2xs.env — его читает systemd-юнит. Оператор удалял пира, доступ исчезал, и ближайший restart возвращал того же пира с тем же секретом. Молча. Признаком стала отметка BOOTSTRAP_PEER_SEEDED в таблице config: «создавался когда-либо», а не «существует сейчас». Отметка и пир пишутся одной транзакцией. P1. Резервная копия с includeSecrets=true проглатывала и ошибку расшифровки, и отсутствие шифртекста, отдавая пира с пустым secret и успешный ответ. Теперь недоступный секрет любого пира отклоняет весь запрос с указанием имени. P1. DecryptPeerSecret возвращала содержимое колонки как расшифрованный секрет, если оно не начиналось с v1: — остаток поколения с открытыми секретами. P1. doctor перезапускал hysteria-server и hy2xs-admin: диагностика подозрения на проблему обрывала все живые соединения. P1. Админка сама генерировала HYSTERIA2_TRAFFIC_STATS_SECRET, записать который в /etc/hysteria/config.yaml она не может. Сервис объявлял себя здоровым, а machine auth переставал совпадать. P1. Обходы проверки зависимостей (accepted-risk/skipped) не могли произвести артефакт: приёмка требует dependency_security_gate=true. Удалены из сборки и документации, отсутствие проверяется приёмкой. P2. UPDATE по отсутствующей строке config считался успехом, и cron перепланировался при несохранённом значении. Решение по RowsAffected. P2. Слой данных не отличал «записи нет» от «база не ответила»: sentinel-значения ErrPeerNotFound / ErrAdminUserNotFound / ErrConfigNotFound / ErrStorage. P2. Удалены алиасы /:id/client-url и /:id/qr. Контракт разработки: apps/go.mod объявляет toolchain go1.26.7 (директива go — языковой baseline, а не выбор компилятора), tools/dev/doctor.sh|.ps1 сверяют среду с versions.env.
57 lines
3.3 KiB
Go
57 lines
3.3 KiB
Go
package dao
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"hy2xs-admin/model/constant"
|
|
)
|
|
|
|
// Отличать «строки нет» от «база не ответила» обязан слой данных.
|
|
//
|
|
// Что было. Каждый Get* возвращал `errors.New(constant.WrongPassword)` для
|
|
// gorm.ErrRecordNotFound и `errors.New(constant.SysError)` для всего
|
|
// остального. Обе — свежие значения ошибки со строкой внутри, поэтому
|
|
// вызывающий мог отличить одно от другого только сравнением текста, чего никто
|
|
// не делал: везде проверялось `err != nil` или `err == nil`.
|
|
//
|
|
// Из-за этого сбой SQLite неотличим от отсутствия записи, а решения на этом
|
|
// принимаются серьёзные:
|
|
//
|
|
// ensureSecureBootstrapPeer — «пира нет» означает «создать пир заново»;
|
|
// applyPeerImportEntry — «по auth_id не нашли» означает «искать по имени»
|
|
// и, не найдя, СОЗДАТЬ запись;
|
|
// ExistPeerName — «ошибка» означает «имя свободно».
|
|
//
|
|
// То есть при недоступной базе продукт не отказывал, а трактовал отказ как
|
|
// разрешение действовать. Транзакция спасает целостность, но не смысл: решение
|
|
// уже принято неверно к моменту записи.
|
|
//
|
|
// Значения объявлены один раз и возвращаются как есть, поэтому работает
|
|
// errors.Is. Тексты сохранены прежними: WrongPassword уезжает в ответ Hysteria
|
|
// при неудачной machine-auth, и менять его здесь было бы изменением внешнего
|
|
// контракта ради внутренней аккуратности.
|
|
var (
|
|
// ErrPeerNotFound — пира с такими условиями в базе нет.
|
|
ErrPeerNotFound = errors.New(constant.WrongPassword)
|
|
|
|
// ErrAdminUserNotFound — учётной записи администратора нет.
|
|
ErrAdminUserNotFound = errors.New(constant.WrongPassword)
|
|
|
|
// ErrConfigNotFound — строки с таким ключом в таблице `config` нет.
|
|
ErrConfigNotFound = errors.New(constant.ConfigNotExist)
|
|
|
|
// ErrStorage — база не смогла ответить. Не «нет данных», а «неизвестно».
|
|
ErrStorage = errors.New(constant.SysError)
|
|
)
|
|
|
|
// IsNotFound — истина только для «записи нет», и никогда для отказа хранилища.
|
|
//
|
|
// Помощник существует, чтобы у вызывающих был ровно один способ задать этот
|
|
// вопрос: три разных sentinel'а для трёх таблиц иначе неизбежно проверялись бы
|
|
// по-разному в разных местах.
|
|
func IsNotFound(err error) bool {
|
|
return errors.Is(err, ErrPeerNotFound) ||
|
|
errors.Is(err, ErrAdminUserNotFound) ||
|
|
errors.Is(err, ErrConfigNotFound)
|
|
}
|