package service import ( "regexp" "strconv" "strings" "unicode/utf8" ) // Контракт учётных данных администратора — ОДИН на весь продукт. // // Зачем он вынесен сюда. Про логин и пароль администратора одновременно знали // четыре места: регексп в слое контроллеров, границы длины в тегах LoginDto, // правило формы входа в панели и правило формы смены пароля в ней же. Ни одно // из них не ссылалось на остальные, и все четыре успели разойтись: // // - тег `validateStr` на Username пережил переименование правила и остался // ссылкой на несуществующую функцию — валидатор паниковал на КАЖДОМ входе, // а gin.Recovery превращал панику в HTTP 500; // - форма входа ограничивала пароль 32 символами, форма смены пароля — 64, // сервер — тоже 64. То есть пароль, назначенный штатной формой смены, // форма входа отказывалась отправлять: панель запирала оператора снаружи // после операции, которую сама же и предложила; // - оркестратор не проверял HY2XS_ADMIN_USER вовсе и по умолчанию писал в // hy2xs.env `admin` — пять символов при минимуме в шесть. Установка // завершалась успешно, а войти в панель было нельзя. // // Общее у всех трёх — контракт существовал только как совпадение литералов, // которое ничто не проверяло. Теперь он объявлен здесь, слой контроллеров зовёт // IsValidAdminUsername, а расхождение с панелью и оркестратором ловится // тестами, читающими эти же константы. const ( AdminUsernameMinLength = 6 AdminUsernameMaxLength = 32 // Границы пароля. Верхняя выбрана не круглым числом: bcrypt читает первые // 72 БАЙТА и молча отбрасывает остаток, поэтому предел обязан быть заведомо // ниже — иначе два разных пароля, совпадающие в начале, окажутся одним и // тем же паролем. AdminPasswordMinLength = 6 AdminPasswordMaxLength = 64 ) // adminUsernameCharacterClass — набор символов логина в записи регекспа. // // Класс записан ЯВНО и повторяет прежнее ФАКТИЧЕСКОЕ множество, включая // последствия неэкранированного дефиса в исходной записи `_+-=`: там `+-=` // образовывал ДИАПАЗОН и впускал `, - . / 0-9 : ; < =`. Сужать набор здесь // нельзя в одиночку — имя администратора приходит из HY2XS_ADMIN_USER, и // установка с логином вроде `admin.ops` перестала бы пускать оператора в // панель. Набор и проверка на стороне оркестратора меняются только вместе. const adminUsernameCharacterClass = `a-zA-Z0-9!@#$%^&*()_+,\-./:;<=` // AdminUsernameCharset — тот же набор в том виде, в каком его показывают // оператору. Объявлен рядом с регекспом, чтобы сообщение об отказе не заводило // собственную копию того же множества. const AdminUsernameCharset = `a-z A-Z 0-9 !@#$%^&*()_+,-./:;<=` // adminUsernamePattern собирается из констант, а не пишется литералом: границы // длины обязаны быть одними и теми же для правила и для сообщения о нём. var adminUsernamePattern = regexp.MustCompile( "^[" + adminUsernameCharacterClass + "]{" + strconv.Itoa(AdminUsernameMinLength) + "," + strconv.Itoa(AdminUsernameMaxLength) + "}$", ) // IsValidAdminUsername сообщает, пригодно ли имя администратора. // // Пробелы по краям снимаются здесь повторно: через форму входа они уже сняты // нормализацией DTO, но эту же функцию зовёт проверка HY2XS_ADMIN_USER, куда // значение приходит из файла окружения, а не из DTO. func IsValidAdminUsername(username string) bool { return adminUsernamePattern.MatchString(strings.TrimSpace(username)) } // IsValidAdminPassword проверяет ТОЛЬКО границы длины. // // Набор символов пароля не ограничен, и это осознанно. Пароль назначает // оператор — установкой через HY2XS_ADMIN_INITIAL_PASSWORD или формой смены, — // и сервер нигде его набор не проверяет. Ограничение на форме входа не // защищает ничего: значение всё равно сравнивается с хешем, — зато отвергает // пароль, который сервер принял бы, то есть умеет только запереть оператора. // // Длина считается в РУНАХ: ровно так её считает go-playground/validator в // правилах `min`/`max` на строке, и расхождение означало бы, что пароль с // кириллицей проходит одну проверку и отказывается на другой. func IsValidAdminPassword(password string) bool { length := utf8.RuneCountInString(password) return length >= AdminPasswordMinLength && length <= AdminPasswordMaxLength }