вход переехал на доверенный заголовок Authelia вместо собственного OIDC

- пришедшего называет заголовок Remote-User от прокси, и верят ему только с
  адреса из перечня trusted_proxies; своего входа у сервиса не осталось — ни
  корня /auth, ни кук, ни срока сессии, ни секрета клиента в конфиге и в базе
- учётная запись заводится первым обращением: EnsureUser в пакете хранилища,
  шаг схемы 202608220001 с колонкой provider_login и снятыми правилами users
- cmd/oidcstub заменён на cmd/devtools с подкомандой proxy; заодно закрыт
  унаследованный DL3066 — пользователь образа назван числом
This commit is contained in:
av
2026-08-22 20:24:22 +03:00
parent e4441f3c49
commit 7f33c957e5
63 changed files with 5257 additions and 2305 deletions
+126
View File
@@ -0,0 +1,126 @@
// Command devtools — оснастка разработчика: то, что нужно для локального
// прогона и никогда не едет в боевой образ.
//
// Пакет один на все такие инструменты, а не по пакету на инструмент. Причина
// счётная: каждый отдельный пакет стоит четырёх мест — строка сборки образа,
// «Деплой» в устройстве, «Команды» в памятке, README, — и забытая строка сборки
// тихо кладёт инструмент разработчика в боевой образ. Один пакет платит эти
// четыре места **однажды**, сколько бы подкоманд в нём ни завелось.
//
// Сегодня подкоманда одна — `proxy`. Заведение владельца панели придёт
// подкомандой `admin` вместе с задачей о запуске одной командой.
//
// Имена заголовков берутся **константами транспорта**, а не литералами: они
// нормативны, и второй список разошёлся бы с первым молча — локальный вход
// перестал бы узнавать кого бы то ни было, а искали бы поломку в сервисе.
//
// Вывод идёт stdlib-логом в поток ошибок, а не `slog`: его читает человек в
// терминале, в сбор он не едет. Изъятие названо строкой в конвенции журнала.
//
// В образ пакет не едет: ступень сборки называет `./cmd/transcriber` поимённо.
package main
import (
"flag"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
httpcontroller "git.vakhrushev.me/av/transcriber/internal/controller/http"
)
func main() {
log.SetFlags(0)
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "proxy":
runProxy(os.Args[2:])
default:
fmt.Fprintf(os.Stderr, "неизвестная подкоманда: %s\n\n", os.Args[1])
usage()
os.Exit(2)
}
}
func usage() {
fmt.Fprint(os.Stderr, `Оснастка разработчика.
Подкоманды:
proxy подставной обратный прокси: ставит заголовок и шлёт запрос сервису
`)
}
// runProxy поднимает подставной обратный прокси.
//
// Зачем он нужен: сервис узнаёт пришедшего по заголовку, который на сервере
// ставит Caddy, сходив к Authelia. На машине разработчика ни того ни другого
// нет, а браузер заголовков не ставит — значит приложение локально не
// открывалось бы вовсе. Прокси встаёт на их место: слушает свой порт, ставит
// заголовок и переправляет запрос сервису.
//
// Проверок он не делает никаких — ни пароля, ни группы, ни срока. Это его
// назначение, а не упущение: пускать он должен всякого, кто до него дошёл, а
// сам он слушает петлевой адрес.
func runProxy(args []string) {
flags := flag.NewFlagSet("proxy", flag.ExitOnError)
listen := flags.String("listen", "127.0.0.1:9000", "адрес, на котором слушать")
target := flags.String("target", "http://127.0.0.1:8080", "адрес сервиса")
login := flags.String("user", "dev", "логин, которым называть пришедшего")
name := flags.String("name", "Разработчик", "имя, пригодное к показу")
email := flags.String("email", "", "адрес почты; пустой не ставится вовсе")
if err := flags.Parse(args); err != nil {
os.Exit(2)
}
// Слушаем петлевой адрес по умолчанию, и это часть назначения: прокси
// пускает всякого, кто до него дошёл, и в чужой сети становится открытым
// входом в сервис.
upstream, err := url.Parse(*target)
if err != nil {
log.Fatalf("адрес сервиса не читается: %v", err)
}
proxy := &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(upstream)
// Именно Set, а не Add. Прокси, который **добавляет** заголовок к
// присланному, оставляет рядом со своим значением чужое — и сервис
// отвергает такой запрос целиком, потому что двух значений он не
// разбирает. Настоящий Caddy обязан делать то же самое, и это
// записано требованием к контуру в модели угроз.
r.Out.Header.Set(httpcontroller.LoginHeader, *login)
r.Out.Header.Set(httpcontroller.NameHeader, *name)
if *email != "" {
r.Out.Header.Set(httpcontroller.EmailHeader, *email)
} else {
r.Out.Header.Del(httpcontroller.EmailHeader)
}
},
}
log.Printf("подставной прокси: %s → %s, пришедший — %q", *listen, *target, *login)
log.Printf("приложение открывать по http://%s", *listen)
server := &http.Server{
Addr: *listen,
Handler: proxy,
// Таймаута чтения нет намеренно: сервис принимает записи на несколько
// часов, и прокси, обрывающий такую загрузку, ловил бы разработчика на
// поломке, которой в сервисе нет.
ReadTimeout: 0,
}
if err := server.ListenAndServe(); err != nil {
log.Fatalf("подставной прокси остановлен: %v", err)
}
}