вход переехал на доверенный заголовок 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
+35 -67
View File
@@ -8,97 +8,65 @@ import (
"time"
)
// Проверка входа — единственная страховка от того, чтобы сервис поднялся с
// молча выключенным входом, то есть открытым наружу. До этих проверок она не
// исполнялась ни разу.
// Проверка перечня доверенных адресов — единственная страховка от того, чтобы
// сервис поднялся никого не узнающим. Узнать о такой поломке было бы неоткуда:
// все адреса приложения просто отвечали бы отказом.
func validAuthConfig() AuthConfig {
return AuthConfig{
AuthURL: "https://auth.example.com/api/oidc/authorization",
TokenURL: "https://auth.example.com/api/oidc/token",
UserInfoURL: "https://auth.example.com/api/oidc/userinfo",
ClientID: "transcriber",
ClientSecret: "secret-value",
RedirectURL: "https://transcriber.example.com/auth/callback",
}
return AuthConfig{TrustedProxies: []string{"172.16.0.0/12", "127.0.0.1"}}
}
func TestAuthConfigValidateAcceptsFilled(t *testing.T) {
if err := validAuthConfig().Validate(); err != nil {
t.Fatalf("заполненный конфиг отвергнут: %v", err)
t.Fatalf("заполненный перечень отвергнут: %v", err)
}
}
func TestAuthConfigValidateNamesEveryMissingKey(t *testing.T) {
cases := map[string]func(*AuthConfig){
"auth_url": func(c *AuthConfig) { c.AuthURL = "" },
"token_url": func(c *AuthConfig) { c.TokenURL = "" },
"user_info_url": func(c *AuthConfig) { c.UserInfoURL = "" },
"client_id": func(c *AuthConfig) { c.ClientID = "" },
"client_secret": func(c *AuthConfig) { c.ClientSecret = "" },
"redirect_url": func(c *AuthConfig) { c.RedirectURL = "" },
}
for key, clear := range cases {
t.Run(key, func(t *testing.T) {
cfg := validAuthConfig()
clear(&cfg)
err := cfg.Validate()
if err == nil {
t.Fatalf("пустой ключ %s пропущен — сервис поднимется с выключенным входом", key)
}
if !strings.Contains(err.Error(), key) {
t.Fatalf("имя ключа %s не названо: %v", key, err)
}
})
}
}
// TestAuthConfigValidateHidesSecretValue: сообщение об отказе уезжает в журнал,
// и значения секрета в нём быть не должно — только имя ключа.
func TestAuthConfigValidateHidesSecretValue(t *testing.T) {
cfg := validAuthConfig()
cfg.ClientSecret = "super-secret-value"
cfg.AuthURL = ""
err := cfg.Validate()
// Пустой перечень значит «не верить никому»: сервис поднялся бы, никого не
// узнавая, и молчать об этом старт не вправе.
func TestAuthConfigValidateRejectsEmptyList(t *testing.T) {
err := AuthConfig{}.Validate()
if err == nil {
t.Fatal("отказа нет")
t.Fatal("пустой перечень принят: сервис поднялся бы никого не узнающим")
}
if strings.Contains(err.Error(), "super-secret-value") {
t.Fatalf("значение секрета попало в текст отказа: %v", err)
if !strings.Contains(err.Error(), "trusted_proxies") {
t.Fatalf("имя ключа не названо: %v", err)
}
}
// TestAuthConfigValidateRejectsMalformedURL: непустая строка, не похожая на
// адрес, отвергается здесь, а не позже — хранилище отказало бы уже из хука
// подъёма, до регистрации пробы здоровья, и сервис упал бы молча целиком.
func TestAuthConfigValidateRejectsMalformedURL(t *testing.T) {
cases := map[string]string{
"без схемы": "auth.example.com/api/oidc/authorization",
"пробел спереди": " https://auth.example.com/authorize",
"чужая схема": "ftp://auth.example.com/authorize",
"пустой хост": "https:///authorize",
"не адрес вовсе": "todo: заполнить",
}
for name, value := range cases {
t.Run(name, func(t *testing.T) {
cfg := validAuthConfig()
cfg.AuthURL = value
// Нечитаемая строка роняет старт: перечень с опечаткой проверяется только тем,
// что кто-то не смог войти.
func TestAuthConfigValidateRejectsMalformedEntry(t *testing.T) {
for _, value := range []string{"", "not-an-address", "10.0.0.0/99", "10.0.0.256"} {
t.Run(value, func(t *testing.T) {
cfg := AuthConfig{TrustedProxies: []string{value}}
err := cfg.Validate()
if err == nil {
t.Fatalf("негодный адрес %q пропущен", value)
t.Fatalf("строка %q принята как адрес", value)
}
if !strings.Contains(err.Error(), "auth_url") {
if !strings.Contains(err.Error(), "trusted_proxies") {
t.Fatalf("имя ключа не названо: %v", err)
}
})
}
}
// Одиночный адрес принимается наравне с подсетью и становится подсетью на один
// адрес: писать разрядность руками значит её помнить, а перечень читает человек.
func TestTrustedNetworksAcceptsBareAddress(t *testing.T) {
networks, err := AuthConfig{TrustedProxies: []string{"10.1.2.3"}}.TrustedNetworks()
if err != nil {
t.Fatalf("одиночный адрес отвергнут: %v", err)
}
if len(networks) != 1 {
t.Fatalf("подсетей %d, ожидалась одна", len(networks))
}
if !networks[0].IsSingleIP() {
t.Fatalf("одиночный адрес стал подсетью шире одного адреса: %s", networks[0])
}
}
func writeConfig(t *testing.T, body string) string {
t.Helper()