локальный вход задаётся конфигом: заголовки подставляет сам сервис
- в конфиг добавлены секция [auth.test_headers] и предохранитель [server] debug: заголовки входа подставляет слой транспорта, второго процесса локальный запуск больше не требует - подкоманда devtools proxy удалена целиком: всё, ради чего её поднимали, делает сам сервис - адресного предохранителя нет по решению владельца — цена названа в ADR и в модели угроз
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
// SubstituteIdentityHeaders — слой отладочного запуска: ставит запросу заголовки
|
||||
// входа значениями из настроек.
|
||||
//
|
||||
// # Зачем он есть
|
||||
//
|
||||
// В бою заголовки ставит обратный прокси, сходивший к провайдеру. На машине
|
||||
// разработчика прокси нет, а браузер заголовков не ставит — и приложение
|
||||
// локально не открылось бы вовсе. Слой встаёт на место контура **внутри самого
|
||||
// сервиса**: второго процесса и второго порта для этого не нужно.
|
||||
//
|
||||
// # Чего слой не делает
|
||||
//
|
||||
// Он правит заголовки запроса и ничего больше: учётной записи не заводит,
|
||||
// отказов не выдаёт, в контекст не пишет. Узнавание получает запрос,
|
||||
// неотличимый от пришедшего с сервера, и отлаживается поэтому боевая ветка, а
|
||||
// не её отладочный двойник.
|
||||
//
|
||||
// # Что ему запрещено
|
||||
//
|
||||
// Подстановка — это настройка, которой сервис называет пришедшего сам, никого
|
||||
// не спросив. Ограничений поэтому три, и каждое отсекает свой способ отдать
|
||||
// чужой архив:
|
||||
//
|
||||
// - пустой перечень значит «подставлять нечего», и слой отдаёт цепочку
|
||||
// нетронутой: звено не встаёт в неё вовсе, а запрос доходит до узнавания
|
||||
// тем же, каким пришёл. Судится пустота здесь, и представление у предиката
|
||||
// «подставляем ли» одно на всех — непустота самого перечня. Иначе
|
||||
// включённый предохранитель при пустой имитации срезал бы `Remote-Email` у
|
||||
// запроса, пришедшего без `Remote-User`;
|
||||
// - подставляется только при **отсутствии** `Remote-User`. Пришедший заголовок
|
||||
// не подменяется и не дополняется: узнавание отвергает запрос с двумя
|
||||
// значениями, и слой, дописавший второе, превратил бы законный отказ в
|
||||
// отладочный проход;
|
||||
// - подставляется только с адреса из перечня доверенных, и судит адрес та же
|
||||
// функция, какой судит его узнавание. Второй сверщик разошёлся бы с первым
|
||||
// молча: узнавание разворачивает IPv4 в оболочке IPv6.
|
||||
//
|
||||
// Подставляя, слой распоряжается **всей** тройкой заголовков входа: названные
|
||||
// настройками ставит их значением, не названные удаляет. Запрос без
|
||||
// `Remote-User`, но с `Remote-Email` иначе собрал бы личность из двух
|
||||
// источников — логин свой, почта чужая.
|
||||
func SubstituteIdentityHeaders(
|
||||
headers map[string]string,
|
||||
trusted []netip.Prefix,
|
||||
logger *slog.Logger,
|
||||
) func(http.Handler) http.Handler {
|
||||
// Подставлять нечего: цепочка слоёв остаётся ровно той, что в бою, —
|
||||
// следующее звено возвращается как есть, лишнего звена не появляется.
|
||||
if len(headers) == 0 {
|
||||
return func(next http.Handler) http.Handler { return next }
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
logger = slog.Default()
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Решает **наличие** заголовка, а не его значение: пустое значение
|
||||
// штатно шлёт прокси там, где никого не назвал, и подстановка на
|
||||
// этом месте назвала бы человеком того, кому провайдер отказал.
|
||||
if len(r.Header.Values(LoginHeader)) > 0 {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
peer, ok := peerAddress(r.RemoteAddr)
|
||||
if !ok || !isTrusted(trusted, peer) {
|
||||
// Своя строка, и уровень предупреждающий. Узнавание здесь
|
||||
// молчит предупреждением: заголовка нет, и оно считает случай
|
||||
// штатным. Без этой строки самый частый локальный отказ —
|
||||
// браузер пришёл с `::1`, а перечень называет `127.0.0.1` — не
|
||||
// отличим от поломки узнавания: человек видит отказ на всём
|
||||
// приложении и ни одного следа о том, почему подстановки не
|
||||
// было.
|
||||
//
|
||||
// Подставляемых значений в строке нет: логин — ключ к чужому
|
||||
// архиву, и запрет печатать его действует на подставленное
|
||||
// значение наравне с пришедшим.
|
||||
logger.Warn("Identity headers are not substituted for an untrusted peer",
|
||||
"http.peer_addr", r.RemoteAddr,
|
||||
"capability", "access", "transport", "http")
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Тройка берётся из своего единственного дома, а не перечисляется
|
||||
// здесь заново.
|
||||
for _, name := range IdentityHeaderNames() {
|
||||
if value, named := headers[name]; named {
|
||||
r.Header.Set(name, value)
|
||||
} else {
|
||||
r.Header.Del(name)
|
||||
}
|
||||
}
|
||||
|
||||
// Уровень `INFO`, и боевой журнал он не топит по построению: строка
|
||||
// пишется только тогда, когда подстановка действительно работает, а
|
||||
// в бою она выключена умолчанием предохранителя. Появляется строка
|
||||
// ровно в одном месте — в локальном прогоне, где стоит на каждом
|
||||
// запросе и где она и нужна: ею «узнан подстановкой» отличается от
|
||||
// «узнан прокси». На отладочном уровне её не видел бы никто:
|
||||
// настройки под уровень журнала у сервиса нет, и он зашит `INFO`.
|
||||
//
|
||||
// Подставленных значений в строке нет по той же причине, по какой их
|
||||
// нет в строке отказа выше.
|
||||
logger.Info("Identity headers substituted from configuration",
|
||||
"http.peer_addr", r.RemoteAddr,
|
||||
"capability", "access", "transport", "http")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// SubstitutedHeaderNames называет заголовки, которые слой подставит, в порядке
|
||||
// объявления тройки.
|
||||
//
|
||||
// Нужен строке журнала при старте: перебор карты дал бы порядок, меняющийся от
|
||||
// запуска к запуску, а перечисление имён по месту завело бы третий список.
|
||||
func SubstitutedHeaderNames(headers map[string]string) []string {
|
||||
names := make([]string, 0, len(headers))
|
||||
for _, name := range IdentityHeaderNames() {
|
||||
if _, named := headers[name]; named {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
Reference in New Issue
Block a user