вход переехал на доверенный заголовок 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
+52 -27
View File
@@ -10,6 +10,7 @@ import (
"mime/multipart"
"net/http"
"net/http/httptest"
"net/netip"
"regexp"
"strings"
"sync"
@@ -72,50 +73,70 @@ type testEnv struct {
handler *AppHandler
app core.App
journal *journalBuffer
// session — значение сессии вошедшего. Приём и опрос закрыты за
// аутентификацией, и проверка, судящая их по существу, обязана предъявить
// сессию ровно так же, как это делает браузер.
session string
// account — учётная запись, которой выдана сессия. Нужна проверкам выхода.
// login — логин у провайдера, которым доверенный источник называет
// пришедшего. Адреса приложения закрыты узнаванием, и проверка, судящая их
// по существу, обязана назваться ровно так же, как это делает прокси.
login string
// account — учётная запись, которой принадлежит этот логин.
account *core.Record
}
// serve шлёт запрос от имени вошедшего: сессия предъявляется кукой — тем же
// способом, каким её предъявляет браузер. Заголовок проверки не ставят: куку в
// него перекладывает слой предъявления, и подмена его здесь означала бы проверку
// не той цепочки.
// Доверенный источник проверок.
//
// Проверки, судящие отказ без сессии, зовут `mux` напрямую.
func (e *testEnv) serve(w http.ResponseWriter, req *http.Request) {
req.AddCookie(&http.Cookie{Name: SessionCookieName, Value: e.session})
e.mux.ServeHTTP(w, req)
// Подсеть и адрес пира разведены намеренно: проверка недоверенного источника
// берёт адрес **вне** подсети, и обе стороны условия видны рядом.
const (
trustedPeer = "10.1.2.3:34567"
untrustedPeer = "203.0.113.9:34567"
)
func testTrustedNetworks(t *testing.T) []netip.Prefix {
t.Helper()
prefix, err := netip.ParsePrefix("10.0.0.0/8")
require.NoError(t, err)
return []netip.Prefix{prefix}
}
// newTestAccount заводит учётную запись и выдаёт ей сессию.
// asUser делает запрос запросом названного человека: ставит заголовок и адрес
// пира из доверенной подсети — ровно то, что делает обратный прокси.
func asUser(req *http.Request, login string) *http.Request {
req.Header.Set(LoginHeader, login)
req.RemoteAddr = trustedPeer
return req
}
// serve шлёт запрос от имени узнанного. Проверки, судящие отказ неузнанному,
// зовут `mux` напрямую: запрос без заголовка и есть запрос неузнанного.
func (e *testEnv) serve(w http.ResponseWriter, req *http.Request) {
e.mux.ServeHTTP(w, asUser(req, e.login))
}
// newTestAccount заводит учётную запись с известным логином.
//
// Запись создаётся прямым сохранением, а не запросом к API: заводить её
// запросом больше нельзя — создание закрыто шагом схемы, и в этом весь смысл
// изменения. Прямое сохранение идёт мимо правил доступа так же, как идёт вход,
// когда учётную запись заводит само хранилище.
// запросом нельзя — создание закрыто шагом схемы. Прямое сохранение идёт мимо
// правил доступа так же, как идёт заведение записи первым обращением.
func newTestAccount(t *testing.T, app core.App) (*core.Record, string) {
t.Helper()
const login = "person"
users, err := app.FindCollectionByNameOrId("users")
require.NoError(t, err)
record := core.NewRecord(users)
record.Set(migrations.ProviderLoginField, login)
record.Set("email", "person@example.com")
record.Set("verified", true)
// Случайный пароль ставит и само хранилище, когда заводит запись по входу у
// провайдера: запись auth-коллекции без пароля не сохраняется, а войти по
// Случайный пароль ставит и сам сервис, когда заводит запись первым
// обращением: запись auth-коллекции без пароля не сохраняется, а войти по
// нему всё равно нельзя — парольный вход выключен шагом схемы.
record.SetRandomPassword()
require.NoError(t, app.Save(record))
token, err := record.NewAuthToken()
require.NoError(t, err)
return record, token
return record, login
}
// journalBuffer — перехваченный журнал одной проверки. Свой на случай: общий на
@@ -191,23 +212,27 @@ func setupTestEnv(t *testing.T, metaviewer contract.AudioMetaViewer) *testEnv {
handler := NewAppHandler(recordRepo, textRepo, pbrepo.NewStructureRepository(app), trsService, logger)
// Роутер собирается тем же способом, что и боевой: маршруты вешает сам
// обработчик, и проверка судит ту же цепочку, что и прод.
// Роутер собирается тем же способом, что и боевой: слой узнавания вешается
// корневым, маршруты вешает сам обработчик, и проверка судит ту же цепочку,
// что и прод. Собери роутер иначе — и проверка судила бы не то.
r, err := apis.NewRouter(app)
require.NoError(t, err)
mounts := ServiceMounts(handler, http.NotFoundHandler())
r.Bind(TrustedHeaderIdentity(app, mounts, testTrustedNetworks(t), logger))
handler.Register(r)
mux, err := r.BuildMux()
require.NoError(t, err)
account, session := newTestAccount(t, app)
account, login := newTestAccount(t, app)
return &testEnv{
mux: mux,
handler: handler,
app: app,
journal: journal,
session: session,
login: login,
account: account,
}
}