config: включение Telegram разведено с ключом доступа
- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет, файл без него негоден; bot_token стал только ключом доступа и при enabled = false не читается вовсе, а пустой при enabled = true роняет старт - выключенный вход даёт подъём одним входом без единого обращения к Telegram и записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение - отказ разбора файла настроек больше не пересказывает toml — её ParseError несёт в тексте разбираемое значение, и оборванная строка секретного ключа уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -41,11 +42,33 @@ type YandexConfig struct {
|
||||
ObjStorageEndpoint string `toml:"object_storage_endpoint"`
|
||||
}
|
||||
|
||||
// TelegramConfig — вход Telegram. Признак включения объявляет намерение
|
||||
// владельца, `BotToken` означает только доступ. Пока два значения жили в одном
|
||||
// поле, пустой токен читался разом как «вход выключен» и как «ключ не доехал»,
|
||||
// и сервис поднимался без бота в обоих случаях.
|
||||
type TelegramConfig struct {
|
||||
// Enabled — умолчания у него нет **намеренно**, и потому его нет в
|
||||
// `defaultConfig()`: умолчание было бы угаданным намерением, а признак
|
||||
// заведён затем, чтобы намерение объявляли. Отсутствие ключа в файле ловит
|
||||
// `LoadConfig` — нулевое значение `bool` режима не выбирает.
|
||||
Enabled bool `toml:"enabled"`
|
||||
BotToken string `toml:"bot_token"`
|
||||
UpdateTimeout int `toml:"update_timeout"`
|
||||
}
|
||||
|
||||
// Validate проверяет ключ доступа против объявленного намерения. Пустой ключ
|
||||
// при включённом входе — ошибка настройки: бот по нему не появится, а тихий
|
||||
// подъём без бота оставил бы отправителей без ответов.
|
||||
//
|
||||
// Названо имя ключа, а не значение: значение `bot_token` в журнал попасть не
|
||||
// должно.
|
||||
func (c TelegramConfig) Validate() error {
|
||||
if c.Enabled && c.BotToken == "" {
|
||||
return errors.New("telegram: не заполнен ключ bot_token при enabled = true")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthConfig — вход через внешнего провайдера OIDC. Адреса, идентификатор
|
||||
// клиента и секрет приезжают сюда и приводятся к настройкам коллекции
|
||||
// пользователей при каждом подъёме: применённый шаг схемы не переписывается, и
|
||||
@@ -129,6 +152,7 @@ func defaultConfig() *Config {
|
||||
ObjStorageRegion: "ru-central1",
|
||||
ObjStorageEndpoint: "https://storage.yandexcloud.net/",
|
||||
},
|
||||
// Умолчания у `Enabled` здесь нет намеренно — причина у поля.
|
||||
Telegram: TelegramConfig{
|
||||
BotToken: "",
|
||||
UpdateTimeout: 10,
|
||||
@@ -149,9 +173,48 @@ func LoadConfig(path string) (*Config, error) {
|
||||
config := defaultConfig()
|
||||
|
||||
// Load configuration from file
|
||||
if _, err := toml.DecodeFile(path, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode config file: %w", err)
|
||||
meta, err := toml.DecodeFile(path, &config)
|
||||
if err != nil {
|
||||
return nil, decodeError(path, err)
|
||||
}
|
||||
|
||||
// Признак включения входа Telegram обязателен: умолчания у него нет, и
|
||||
// отличить «не задан» от «задан ложным» умеет только разбор — нулевое
|
||||
// значение `bool` в структуре у обоих одинаковое. Отсюда и `meta`: наружу
|
||||
// она не отдаётся, приговор выносится здесь.
|
||||
if !meta.IsDefined("telegram", "enabled") {
|
||||
return nil, errors.New("telegram: не задан ключ enabled; он объявляет, нужен ли сервису вход Telegram")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// decodeError переводит отказ разбора на свои слова. Пересказывать библиотеку
|
||||
// нельзя: она собирает текст отказа из разбираемого куска файла, и оборванная
|
||||
// строка секретного ключа уехала бы в журнал вместе со значением.
|
||||
//
|
||||
// Разрез идёт по семейству отказа, и значения несёт только одно:
|
||||
//
|
||||
// - `toml.ParseError` — сюда сведены отказы лексера и разбора значения, а его
|
||||
// `Message` собран из разбираемого куска («Invalid float value: %q»,
|
||||
// «invalid duration: %q»). Берём строку, столбец и последний ключ — они
|
||||
// безопасны, — а `Message` не берём;
|
||||
// - прочие отказы декодера собраны из имён ключей и имён типов, значений в них
|
||||
// нет вовсе. Их текст берём как есть: выбросив его, мы заплатили бы
|
||||
// разборчивостью отказа там, где платить не за что.
|
||||
//
|
||||
// Две ветки не сводятся в одну намеренно. Сведённая к общему знаменателю, она
|
||||
// либо вернёт утечку, либо оставит несовпадение типов без единого намёка.
|
||||
func decodeError(path string, err error) error {
|
||||
var parseErr toml.ParseError
|
||||
if errors.As(err, &parseErr) {
|
||||
if parseErr.LastKey != "" {
|
||||
return fmt.Errorf("config file %s: разбор оборвался на строке %d, столбце %d, последний ключ %q",
|
||||
path, parseErr.Position.Line, parseErr.Position.Col, parseErr.LastKey)
|
||||
}
|
||||
return fmt.Errorf("config file %s: разбор оборвался на строке %d, столбце %d",
|
||||
path, parseErr.Position.Line, parseErr.Position.Col)
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to decode config file %s: %w", path, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user