b22b4b0d99
Два свойства были описаны в документации, но не обеспечены кодом.
1. doctor «не изменяет диагностируемую систему».
Принудительный skipServiceStart закрывал ровно одну ИЗВЕСТНУЮ мутацию —
рестарт сервисов. Всё остальное в smoke держалось на том, что автор правки
выбрал правильный раннер: `test -s`, `grep -q`, `stat`, `sudo -u ... test`
и `nft -c` шли через мутирующий namespace, хотя ничего не меняют. Ожидание
между попытками выполнялось подпроцессом `sleep` через runMutatingHidden,
то есть пауза между двумя чтениями объявлялась изменением системы.
Следствие: настоящая мутация, случайно добавленная в smoke, ничем бы от них
не отличалась и была бы разрешена в doctor молча — а включить guard было
нельзя, он отказал бы на первой же читающей команде.
Команды классифицированы честно, `sleep` заменён таймером, и doctor целиком
выполняется под тем же read-only guard, что и PHASE 0 установки. Guard
снимается в finally. Диагностика при этом не сузилась: слушатели, healthz,
права, machine auth, trafficStats, версия бинаря, семантика конфига и
синтаксис nft проверяются полностью.
2. reset-admin различает «администратора нет» и «база не ответила».
Слой данных специально возвращает разные sentinel'ы, но команда склеивала их
обычным `if err != nil { создать } else { обновить }`. Опасен здесь не
только нарушенный смысл: при транзиентном отказе чтения («database is
locked») ветка создания отрабатывала успешно, и в таблице оказывались ДВЕ
учётные записи администратора. GetAdminUser берёт First() и о второй строке
не сообщает — на сервере оставалась вторая рабочая учётка с паролем, уже
напечатанным на экран, и ни один запрос об этом не говорил.
Заодно исправлено проглатывание ошибки хеширования: в ветке обновления
стояло `hash, _ := util.HashPassword(password)` внутри литерала map. При
отказе bcrypt в password_hash уезжала пустая строка, а на экран печатался
пароль, которым войти уже невозможно — VerifyPassword отклоняет всё, что не
bcrypt. Команда восстановления доступа умела молча его отобрать.
Тесты: doctor-readonly.test.ts дополнен поведенческой проверкой guard и
контролем набора раннеров в smoke; apps/cmd/reset_test.go проверяет обе ветки
на настоящей SQLite и отказ чтения при полностью работоспособной базе — ровно
тот случай, который прежний код превращал во второго администратора. Добавлена
dao.CountAdminUsers: до неё появление дубликата было ненаблюдаемым.
58 lines
2.0 KiB
Go
58 lines
2.0 KiB
Go
package dao
|
|
|
|
import (
|
|
"errors"
|
|
"github.com/sirupsen/logrus"
|
|
"gorm.io/gorm"
|
|
"hy2xs-admin/model/entity"
|
|
"time"
|
|
)
|
|
|
|
func GetAdminUser(query interface{}, args ...interface{}) (entity.AdminUser, error) {
|
|
var admin entity.AdminUser
|
|
if tx := sqliteDB.Model(&entity.AdminUser{}).Where(query, args...).First(&admin); tx.Error != nil {
|
|
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
|
|
return admin, ErrAdminUserNotFound
|
|
}
|
|
logrus.Errorf("%v", tx.Error)
|
|
return admin, ErrStorage
|
|
}
|
|
return admin, nil
|
|
}
|
|
|
|
// CountAdminUsers — сколько учётных записей администратора существует.
|
|
//
|
|
// Продукт допускает ровно одну, и это ЕДИНСТВЕННОЕ место, где такой вопрос
|
|
// можно задать: `GetAdminUser` берёт First() и о наличии второй строки не
|
|
// сообщает. Именно поэтому появление дубликата (ветка создания, выбранная при
|
|
// отказе чтения) было ненаблюдаемым — вторая рабочая учётка с уже напечатанным
|
|
// на экран паролем просто существовала, и никакой запрос об этом не говорил.
|
|
func CountAdminUsers() (int64, error) {
|
|
var count int64
|
|
if tx := sqliteDB.Model(&entity.AdminUser{}).Count(&count); tx.Error != nil {
|
|
logrus.Errorf("%v", tx.Error)
|
|
return 0, ErrStorage
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func SaveAdminUser(admin entity.AdminUser) (int64, error) {
|
|
if tx := sqliteDB.Save(&admin); tx.Error != nil {
|
|
logrus.Errorf("%v", tx.Error)
|
|
return 0, ErrStorage
|
|
}
|
|
return *admin.Id, nil
|
|
}
|
|
|
|
func UpdateAdminUser(ids []int64, updates map[string]interface{}) error {
|
|
if len(updates) == 0 {
|
|
return nil
|
|
}
|
|
updates["update_time"] = time.Now().Format("2006-01-02 15:04:05")
|
|
if tx := sqliteDB.Model(&entity.AdminUser{}).Where("id in ?", ids).Updates(updates); tx.Error != nil {
|
|
logrus.Errorf("%v", tx.Error)
|
|
return ErrStorage
|
|
}
|
|
return nil
|
|
}
|