tasks: вход переезжает на заголовки прокси, пять задач про OIDC закрыты

- заведена `trusted-header-login` и поставлена в голову очереди
- пять задач про механику OIDC закрыты как отменённые ею
- `dev-run-task` и `api-tokens` переписаны под новый вход
This commit is contained in:
av
2026-08-22 17:43:18 +03:00
parent 2bb252e018
commit e4441f3c49
10 changed files with 113 additions and 261 deletions
+3 -7
View File
@@ -43,21 +43,17 @@
## Очередь ## Очередь
- [🧹 Поднимать сервис и заглушку одной командой](items/dev-run-task.md) — Локальная проверка требует двух терминалов, ручной остановки обоих процессов и чистки каталога данных; учётную запись заводят проходом по браузеру, а владельца панели — по одноразовой ссылке из журнала. - [ Пускать по доверенным заголовкам Authelia вместо входа OIDC](items/trusted-header-login.md) — Вход собран своими руками — состояние, PKCE, обмен кода внутрипроцессным запросом к роутеру хранилища, — а обратный прокси уже отдаёт трём соседним сервисам заголовки Remote-* от Authelia; заодно секрет клиента лежит в базе, а панель обходится подменой знака в пути.
- [🧹 Поднимать сервис для локальной работы одной командой](items/dev-run-task.md) — Локальная проверка требует ручной чистки каталога данных и остановки процесса, а владельца панели заводят по одноразовой ссылке из журнала.
- [✨ Сделать экран загрузки записи и её состояния](items/upload-and-status-screen.md) — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит. - [✨ Сделать экран загрузки записи и её состояния](items/upload-and-status-screen.md) — Первое, ради чего приложение открывают: отдать файл и увидеть, что с ним происходит.
- [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем. - [✨ Сделать экран списка своих записей и чтения текста](items/records-list-screen.md) — Расшифровка сегодня доходит одним сообщением и теряется в переписке; вернуться к ней через неделю нечем.
- [✨ Дать владельцу править запись, возвращать её в работу и видеть её путь](items/audiorecord-actions.md) — С записью нельзя сделать ничего: заголовок ставит одна языковая модель, остановленную возвращает в работу только владелец сервиса в панели, а журнал событий пишется и не читается никем, кроме него же - [✨ Дать владельцу править запись, возвращать её в работу и видеть её путь](items/audiorecord-actions.md) — С записью нельзя сделать ничего: заголовок ставит одна языковая модель, остановленную возвращает в работу только владелец сервиса в панели, а журнал событий пишется и не читается никем, кроме него же
- [🐞 Убрать код провайдера из журнала запросов хранилища](items/provider-code-out-of-storage-log.md) — Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал.
- [🐞 Вести учёт употреблённых состояний входа на сервере](items/server-side-login-state.md) — Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены.
- [✨ Строить адрес входа из настроек коллекции, а не из конфига](items/login-url-from-collection-settings.md) — Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину.
- [🔬 Четыре недоказанные гипотезы о поверхности входа](items/login-surface-hypotheses.md) — Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было.
- [🐞 Починить срок сессии, который ставит откат шага входа](items/rollback-restores-wrong-session-duration.md) — Константа defaultAuthTokenDuration в шаге 202608120001 названа умолчанием библиотеки, но 1209600 — это 14 суток, а умолчание PocketBase 432000, пять суток: откат объявляет возврат к умолчанию и ставит срок вдвое больше выбранных владельцем семи.
- [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись. - [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись.
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в cmd/transcriber проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом. - [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в cmd/transcriber проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
- [🧹 Запретить обращаться к Bot API мимо клиента бота](items/bot-api-only-through-bot-client.md) — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта. - [🧹 Запретить обращаться к Bot API мимо клиента бота](items/bot-api-only-through-bot-client.md) — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
- [🧹 Свести пять расхождений между документами канона](items/docs-consistency-2026-08-13.md) — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; одно сведено при повышении раскладки, а три из пяти оставшихся стоят в architecture.md, и по ним читатель строит решения о выкладке и о периметре. - [🧹 Свести пять расхождений между документами канона](items/docs-consistency-2026-08-13.md) — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; одно сведено при повышении раскладки, а три из пяти оставшихся стоят в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны. - [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
- [✨ Пускать скрипты в API по личным токенам](items/api-tokens.md) — Вход через OIDC закрывает API целиком, а скрипту браузерная сессия недоступна: автоматизировать загрузку станет нечем. - [✨ Пускать скрипты в API по личным токенам](items/api-tokens.md) — Скрипту недоступны ни браузерная сессия, ни вход у Authelia: домен целиком стоит за прокси, и автоматизировать загрузку нечем.
- [🧹 Покрыть тестами шаги конвейера и захват задачи](items/pipeline-step-tests.md) — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем. - [🧹 Покрыть тестами шаги конвейера и захват задачи](items/pipeline-step-tests.md) — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем.
- [🧹 Покрыть тестами разбор вывода ffprobe](items/metaviewer-adapter-tests.md) — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто. - [🧹 Покрыть тестами разбор вывода ffprobe](items/metaviewer-adapter-tests.md) — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто.
- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. - [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
+5
View File
@@ -26,3 +26,8 @@
- 2026-08-15 `resume-halted-record` — ✨ Вернуть остановленную запись в работу из приложения. Причина: Слилась в audiorecord-actions 2026-08-15: возврат в работу живёт на экране одной записи вместе с правкой заголовка и журналом событий. Была секция: Очередь. - 2026-08-15 `resume-halted-record` — ✨ Вернуть остановленную запись в работу из приложения. Причина: Слилась в audiorecord-actions 2026-08-15: возврат в работу живёт на экране одной записи вместе с правкой заголовка и журналом событий. Была секция: Очередь.
- 2026-08-15 `record-events-screen` — ✨ Показывать журнал событий записи её владельцу. Причина: Слилась в audiorecord-actions 2026-08-15: журнал событий читается с экрана одной записи вместе с правкой заголовка и возвратом в работу. Была секция: Очередь. - 2026-08-15 `record-events-screen` — ✨ Показывать журнал событий записи её владельцу. Причина: Слилась в audiorecord-actions 2026-08-15: журнал событий читается с экрана одной записи вместе с правкой заголовка и возвратом в работу. Была секция: Очередь.
- 2026-08-15 `admin-owner-from-config` — ✨ Заводить владельца панели по ключу конфига. Причина: Решение владельца 2026-08-15: владелец панели нужен только для локальной проверки, и заводит его отдельная команда cmd/devadmin — инструмент разработчика рядом с cmd/oidcstub. Ключ конфига не заводится, требование спеки storage «Пароль владельца от панели не лежит в конфигурации» остаётся в силе. Проверено прогоном: команда открывает базу тем же pbrepo.New, накатывает схему целиком, пароль существующего владельца не переписывает, на настройки провайдера OIDC не влияет — их сервис применяет из конфига при каждом подъёме. Была секция: Очередь. - 2026-08-15 `admin-owner-from-config` — ✨ Заводить владельца панели по ключу конфига. Причина: Решение владельца 2026-08-15: владелец панели нужен только для локальной проверки, и заводит его отдельная команда cmd/devadmin — инструмент разработчика рядом с cmd/oidcstub. Ключ конфига не заводится, требование спеки storage «Пароль владельца от панели не лежит в конфигурации» остаётся в силе. Проверено прогоном: команда открывает базу тем же pbrepo.New, накатывает схему целиком, пароль существующего владельца не переписывает, на настройки провайдера OIDC не влияет — их сервис применяет из конфига при каждом подъёме. Была секция: Очередь.
- 2026-08-22 `provider-code-out-of-storage-log` — 🐞 Убрать код провайдера из журнала запросов хранилища. Причина: Кода провайдера не станет: вход переезжает на доверенные заголовки задачей trusted-header-login, адрес возврата исчезает вместе с корнем /auth, и в журнал запросов хранилища попадать нечему. Была секция: Очередь.
- 2026-08-22 `server-side-login-state` — 🐞 Вести учёт употреблённых состояний входа на сервере. Причина: Состояния входа не станет: сверять возврат будет нечего — вход переезжает на доверенные заголовки задачей trusted-header-login, куки состояния и обмена кода исчезают. Была секция: Очередь.
- 2026-08-22 `login-url-from-collection-settings` — ✨ Строить адрес входа из настроек коллекции, а не из конфига. Причина: Адреса входа не станет вовсе, и настроек провайдера у коллекции тоже: их кладёт ApplyProviderSettings, которую убирает trusted-header-login. Расхождение двух половин протокола исчезает вместе с протоколом. Была секция: Очередь.
- 2026-08-22 `login-surface-hypotheses` — 🔬 Четыре недоказанные гипотезы о поверхности входа. Причина: Все четыре пути закрывает trusted-header-login: выход по чужой ссылке и слияние двух записей провайдера с одной почтой исчезают вместе с собственным входом, поле снимка — вместе с ответом провайдера, а анонимный запрос подтверждения почты — вместе с собственными адресами входа хранилища, которые та же задача глушит по перечню. Была секция: Очередь.
- 2026-08-22 `rollback-restores-wrong-session-duration` — 🐞 Починить срок сессии, который ставит откат шага входа. Причина: Константа живёт в откате шага 202608120001_oidc_login, а срок сессии перестаёт значить что-либо: сессии хранилища не будет — доступ судит Authelia на каждом запросе. Шаг остаётся применённым и не переписывается, откаты по-прежнему ничем не подключены. Была секция: Очередь.
+11 -3
View File
@@ -2,17 +2,24 @@
- **Тип:** feature - **Тип:** feature
- **Категория:** Очередь — Второй способ представиться ставится на готовые владельца и контракт, иначе форма ошибки переписывается дважды. - **Категория:** Очередь — Второй способ представиться ставится на готовые владельца и контракт, иначе форма ошибки переписывается дважды.
- **Зачем:** Вход через OIDC закрывает API целиком, а скрипту браузерная сессия недоступна: автоматизировать загрузку станет нечем. - **Зачем:** Скрипту недоступны ни браузерная сессия, ни вход у Authelia: домен целиком стоит за прокси, и автоматизировать загрузку нечем.
Запрос без токена не проходит, а скрипт ходит в API по токену, выпущенному Запрос без токена не проходит, а скрипт ходит в API по токену, выпущенному
пользователем, и видит ровно его записи. пользователем, и видит ровно его записи.
Половина работы при этом лежит на контуре: домен целиком стоит за `forward_auth`
Authelia, и запрос скрипта не доходит до сервиса вовсе — его отбивает прокси.
Значит адресам API нужно правило, пропускающее их к сервису, а сервис судит
токен сам. Правило живёт в `pet-project-server`, вне этого репозитория.
Токен принадлежит учётной записи и даёт ровно её права: записи, заведённые по Токен принадлежит учётной записи и даёт ровно её права: записи, заведённые по
токену, видны владельцу в приложении, и наоборот. токену, видны владельцу в приложении, и наоборот.
## Затрагивает ## Затрагивает
- заголовок авторизации у всех адресов приложения `/app/`; - заголовок авторизации у всех адресов приложения `/app/` и его отношение к
заголовку, который ставит прокси: два способа представиться и один владелец;
- правило Authelia, пропускающее адреса API мимо входа, — в `pet-project-server`;
- таблица токенов: владелец, имя, отпечаток, время выпуска и последнего - таблица токенов: владелец, имя, отпечаток, время выпуска и последнего
обращения, и её миграция; обращения, и её миграция;
- адреса выпуска, перечня и отзыва токена — `/app/me/tokens`; - адреса выпуска, перечня и отзыва токена — `/app/me/tokens`;
@@ -35,6 +42,7 @@
Учётные записи по-прежнему заводит Authelia — свою регистрацию не делаем. Учётные записи по-прежнему заводит Authelia — свою регистрацию не делаем.
Сроков жизни и областей действия у токена не заводим: он даёт права владельца Сроков жизни и областей действия у токена не заводим: он даёт права владельца
целиком. Берётся после `oidc-login`: до неё представляться некому. Экрана выпуска здесь целиком. Берётся после `trusted-header-login`: до неё представляться некому, а
она же решает, какому источнику сервис верит. Экрана выпуска здесь
нет — приложения ещё не существует, токен выпускается запросом к API; место нет — приложения ещё не существует, токен выпускается запросом к API; место
токена на экране настроек заводит `settings-screen`. токена на экране настроек заводит `settings-screen`.
+22 -18
View File
@@ -1,18 +1,22 @@
# 🧹 Поднимать сервис и заглушку одной командой # 🧹 Поднимать сервис для локальной работы одной командой
- **Тип:** chore - **Тип:** chore
- **Категория:** Очередь — оснастка локального прогона: без неё каждую проверку начинают с двух терминалов и ручной уборки - **Категория:** Очередь — оснастка локального прогона: без неё каждую проверку начинают с двух терминалов и ручной уборки
- **Зачем:** Локальная проверка требует двух терминалов, ручной остановки обоих процессов и чистки каталога данных; учётную запись заводят проходом по браузеру, а владельца панели по одноразовой ссылке из журнала. - **Зачем:** Локальная проверка требует ручной чистки каталога данных и остановки процесса, а владельца панели заводят по одноразовой ссылке из журнала.
Шаг `task dev CONFIG=<путь> USER=<почта>` поднимает сервис и подставной Шаг `task dev CONFIG=<путь> USER=<имя>` поднимает сервис, кладёт данные во
провайдер OIDC разом, кладёт данные во временный каталог, заводит учётные записи временный каталог, заводит владельца панели и гасит процесс по Ctrl+C.
и гасит оба процесса по Ctrl+C.
Работа состоит из двух частей, и они едут одним коммитом: инструмент Работа состоит из двух частей, и они едут одним коммитом: инструмент
`cmd/devadmin` и шаг `dev`, который его зовёт. Порознь они не нужны — шаг без `cmd/devadmin` и шаг `dev`, который его зовёт. Порознь они не нужны — шаг без
инструмента не заведёт владельца панели, инструмент без шага останется вызовом, инструмента не заведёт владельца панели, инструмент без шага останется вызовом,
который никто не делает. который никто не делает.
Заглушки провайдера здесь больше нет: вход переезжает на доверенные заголовки
задачей `trusted-header-login`, и обычная учётная запись заводится первым же
запросом с заголовком. Каким способом заголовок попадает в запрос на машине без
прокси, решает та задача; этот шаг её решение только зовёт.
Задуманное устройство: Задуманное устройство:
- конфиг пишет человек и передаёт путём — шаг его не сочиняет; - конфиг пишет человек и передаёт путём — шаг его не сочиняет;
@@ -21,13 +25,12 @@
того, что написано в конфиге. Временное живёт в `/tmp` — в `data/` писать того, что написано в конфиге. Временное живёт в `/tmp` — в `data/` писать
запрещено (CLAUDE.md, «Запреты»); запрещено (CLAUDE.md, «Запреты»);
- владельца панели заводит `cmd/devadmin` — отдельный main-пакет рядом с - владельца панели заводит `cmd/devadmin` — отдельный main-пакет рядом с
`cmd/oidcstub`, в образ он не едет. Базу открывает тем же `pbrepo.New`, что и `cmd/transcriber`, в образ он не едет. Базу открывает тем же `pbrepo.New`, что и
сервис, и потому получает её со схемой целиком; зовётся до подъёма сервиса, на сервис, и потому получает её со схемой целиком; зовётся до подъёма сервиса, на
пустом каталоге. Пароль существующего владельца не переписывает; пустом каталоге. Пароль существующего владельца не переписывает;
- шаг заводит обычную учётную запись входом: поднимает `cmd/oidcstub` с - обычная учётная запись заводится первым запросом с заголовком: имя берётся из
`-email` из `USER`, сам проходит цепочку входа и тем создаёт запись; `USER`, и заводить её отдельно шагу не нужно;
- оба процесса идут одной группой, `trap` на `INT` и `TERM` гасит их разом, - `trap` на `INT` и `TERM` гасит процесс, `wait` держит терминал.
`wait` держит терминал.
## Затрагивает ## Затрагивает
@@ -41,14 +44,14 @@
## Критерии приёмки ## Критерии приёмки
- Команда поднимает оба процесса и печатает адрес, по которому открывать - Команда поднимает сервис и печатает адрес, по которому открывать приложение,
приложение. Оракул: прогон с тестовым конфигом — сервис и заглушка слушают свои вместе со способом представиться. Оракул: прогон с тестовым конфигом — сервис
порты. слушает свой порт, названный адрес отвечает.
- Ctrl+C гасит обоих и не оставляет висящих процессов. Оракул: `pgrep` по обоим - Ctrl+C гасит процесс и не оставляет висящих. Оракул: `pgrep` по имени после
именам после остановки — пусто. остановки — пусто.
- После запуска в базе есть учётная запись с переданной почтой, а владелец панели - Владелец панели входит своим паролем, а первый запрос с именем из `USER`
входит своим паролем. Оракул: запрос к коллекции пользователей во временной базе заводит учётную запись. Оракул: вход по `auth-with-password` в коллекцию
и вход по `auth-with-password` в коллекцию владельцев. владельцев и запрос к коллекции пользователей во временной базе.
- Каталог данных лежит во временном месте, а `data/` не тронут. Оракул: путь базы - Каталог данных лежит во временном месте, а `data/` не тронут. Оракул: путь базы
из журнала подъёма и `git status` после прогона. из журнала подъёма и `git status` после прогона.
- `task gate` остаётся зелёным, скрипт проходит `shellcheck`, а образ не получает - `task gate` остаётся зелёным, скрипт проходит `shellcheck`, а образ не получает
@@ -57,6 +60,7 @@
## Рамки ## Рамки
Шаг локальный: наружу не ходит, боевых ключей не требует и в образ не попадает. Шаг локальный: наружу не ходит, боевых ключей не требует и в образ не попадает.
Идёт после `trusted-header-login` — до неё локальный вход устроен иначе.
Пароль владельца в конфиг **не заводится** — требование спеки `storage` «Пароль Пароль владельца в конфиг **не заводится** — требование спеки `storage` «Пароль
владельца от панели не лежит в конфигурации» остаётся в силе, и работа его не владельца от панели не лежит в конфигурации» остаётся в силе, и работа его не
трогает. Распознавание при выдуманных ключах не работает — это остаётся как есть, трогает. Распознавание при выдуманных ключах не работает — это остаётся как есть,
-44
View File
@@ -1,44 +0,0 @@
# 🔬 Четыре недоказанные гипотезы о поверхности входа
- **Тип:** research
- **Категория:** Очередь — Разведка закрывает тему входа последней: остальные три задачи меняют то, что она проверяет.
- **Зачем:** Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было.
Откуда — отчёт триажа ревью задачи `oidc-login` 2026-08-12,
[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md),
раздел «Гипотезы без доказательства». Каждая либо становится задачей, либо
закрывается с причиной; сегодня они не то и не другое.
## Вопрос
Работает ли хоть один из четырёх путей на самом деле, и если да — чего стоит
каждый?
1. **Выход по чужой ссылке.** Адрес выхода сессии не требует и на запросе с
чужого сайта отвечает успехом, убирая куку. Применит ли браузер эту куку при
ограничении `SameSite=Lax` — по коду не выяснить. Если применит, человека
выкидывает молча, а унесённое значение остаётся годным.
2. **Слияние двух учётных записей провайдера с одной почтой.** Обмен ищет запись
по неизменяемому признаку провайдера, а не найдя — по адресу почты. Выдаст ли
Authelia двум разным субъектам один адрес, зависит от её настройки.
3. **Поле снимка в ответе провайдера тянет данные наружу.** Оно сопоставлено
файловому полю учётной записи, и библиотека скачивает названный там адрес —
до потолка размера записи. Шлёт ли Authelia это поле и кто им управляет,
неизвестно.
4. **Анонимный запрос подтверждения почты.** Адрес отвечает успехом и заставляет
сервис слать почту. Почта не настроена, и потолка числа запросов нет.
## Куда ляжет ответ
- подтверждённый путь — задачей в беклоге, и она называет эту разведку;
- опровергнутый — строкой в `docs/security.md`, раздел «Что вне модели» либо
«Что разграничивает доступ», чтобы следующее ревью не открывало его заново;
- то, что зависит от настройки Authelia, — строкой там же, с указанием, какая
именно настройка это решает.
## Рамки
Смотрим только четыре названных пути. Первый требует настоящего браузера, второй
и третий — настоящей Authelia либо её настройки из `pet-project-server`;
четвёртый воспроизводится своим прогоном без внешних систем и потому берётся
первым. Прогонов на боевом контуре не делаем.
@@ -1,38 +0,0 @@
# ✨ Строить адрес входа из настроек коллекции, а не из конфига
- **Тип:** feature
- **Категория:** Очередь — Замыкает тройку правок обработчиков входа.
- **Зачем:** Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину.
Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа —
[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md),
пункт срезанного потолком под номером 7.
Сегодня адрес согласия собирается своим кодом: состав запрашиваемых сведений,
способ проверочного кода и признак ответа записаны у нас, а обмен кода берёт
настройки провайдера из коллекции хранилища. Библиотека умеет собирать этот
адрес сама — она делает это своим обработчиком способов входа.
Цена расхождения отложенная: адреса провайдера и идентификатор клиента получают
второго потребителя мимо единственного места, где настройки живут, а правка
провайдера в панели на начало входа не влияет вовсе. Обновление библиотеки,
тронувшее форму запроса согласия, доедет до половины протокола и разойдётся
молча — отказом на живой выкладке, которого нечем воспроизвести.
## Затрагивает
- эндпоинт `GET /auth/login`, где сегодня адрес согласия собирается вручную;
- поля `auth_url` и `client_id` в секции `[auth]` конфига и их проброс —
часть из них перестаёт быть нужной приложению;
- дельта-спека `access`, требование «Вход через внешнего провайдера» — состав
запрашиваемых сведений и способ проверочного кода перестают быть нашими.
## Критерии приёмки
- Адрес согласия строится из настроек коллекции: правка провайдера в панели
меняет адрес, куда уводит вход. Оракул — тест: сменить настройки провайдера в
хранилище и убедиться, что адрес перенаправления изменился.
- Проверочный код и состав запрашиваемых сведений берутся у библиотеки, а не
записаны у нас. Оракул — чтение кода: своих литералов состава больше нет.
- Вход по-прежнему проходит целиком. Оракул — существующий тест входа через
подставного провайдера остаётся зелёным.
@@ -1,44 +0,0 @@
# 🐞 Убрать код провайдера из журнала запросов хранилища
- **Тип:** fix
- **Категория:** Очередь — Три задачи входа собраны подряд: все правят обработчики входа и одно требование спеки access — три захода в один файл дороже одного.
- **Зачем:** Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал.
Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа —
[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md),
пункт срезанного потолком под номером 2.
Наш собственный журнал чист — код туда не пишет ни одна строка приложения.
Пишет его слой хранилища: он логирует всякий запрос вместе со строкой запроса,
а адрес возврата несёт код параметром. Тест на отсутствие значений в журнале
этого не видит, потому что смотрит только в наш логгер.
Код провайдера одноразовый и живёт минуты, поэтому это не захват сессии, а
расхождение написанного со сделанным: комментарий в коде и норма спеки
утверждают, что код в журнал не идёт.
## Воспроизведение
1. Поднять сервис с настроенным провайдером.
2. Пройти вход и вернуться на адрес возврата.
3. Открыть журнал запросов хранилища — панель, `GET /api/logs` либо файл базы.
4. В строке запроса и в поле `url` виден код провайдера целиком.
## Затрагивает
- адрес возврата `GET /auth/callback` и слой журналирования запросов хранилища;
- настройка срока хранения журнала запросов (сегодня умолчание, пять суток);
- дельта-спека `access`, требование «Значение, дающее доступ, не печатается» —
либо норма выполняется, либо изъятие называется поимённо;
- `docs/security.md`, перечень мест, где оседает чувствительное.
## Критерии приёмки
- После входа код провайдера не встречается в журнале запросов хранилища.
Оракул — тест: пройти вход подставным провайдером, затем отобрать записи
журнала и убедиться, что значения кода в них нет.
- Строка о запросе к адресу возврата в журнале остаётся: пропажа самого следа
не годится, прослеживаемость входа нужна. Оракул — тот же тест: запись о
запросе есть, кода в ней нет.
- Норма и код сошлись: либо спека выполняется буквально, либо в ней названо
изъятие с ценой. Оракул — чтение требования против исхода первого теста.
@@ -1,53 +0,0 @@
# 🐞 Починить срок сессии, который ставит откат шага входа
- **Тип:** fix
- **Категория:** Очередь — Оба про откат шага схемы: соседняя строка называет, что откат не отменяет применённое, эта чинит константу, которую он ставит. Порознь их правят дважды в одном файле.
- **Зачем:** Константа defaultAuthTokenDuration в шаге 202608120001 названа умолчанием библиотеки, но 1209600 — это 14 суток, а умолчание PocketBase 432000, пять суток: откат объявляет возврат к умолчанию и ставит срок вдвое больше выбранных владельцем семи.
Найдено ревью пачки задач о гейте 2026-08-12, проход разбора кода. Дефект
пришёл из задачи `oidc-login` и её правкой не был замечен; переезд шагов схемы в
свой каталог развёл противоречащие комментарии по разным файлам, где раньше они
стояли в трёх строках друг от друга: `provider.go` пишет «умолчание библиотеки в
пять суток», шаг — «умолчание библиотеки» о числе в 14 суток.
Оракул сегодняшнего состояния:
`grep -n "Duration: 432000" ~/go/pkg/mod/github.com/pocketbase/pocketbase@v0.39.10/core/collection_model_auth_options.go`
отдаёт строку `Duration: 432000, // 5days`. Числа `1209600` в модуле нет ни разу
как длительности токена.
**Развилка, которую решает человек:** правка меняет поведение **применённого**
шага схемы, а такое спрашивается всегда. Либо константа приводится к 432000
внутри существующего шага (откат ещё не исполнялся ни разу, и переписывание
трогает только будущие откаты), либо 14 суток объявляются выбранным числом — и
тогда меняется имя константы и её комментарий, а не значение.
## Воспроизведение
1. Подключить откаты (`migratecmd`) — сегодня их не подключает ничто.
2. Откатить шаг `202608120001_oidc_login.go`.
3. Прочитать `AuthToken.Duration` коллекции `users`: там 1209600 — вдвое больше
семи суток, выбранных владельцем, и в 2.8 раза больше умолчания библиотеки,
к которому откат обещал вернуть.
## Затрагивает
- `internal/adapter/repo/pocketbase/migrations/202608120001_oidc_login.go`,
константа `defaultAuthTokenDuration` и комментарий `down202608120001`;
- `internal/adapter/repo/pocketbase/provider.go`, комментарий к
`SessionDuration` — он называет умолчание пятью сутками;
- поведение откатов, если их подключат.
## Критерии приёмки
- Число в откате и число, названное умолчанием библиотеки, совпадают. Оракул —
чтение константы против `core/collection_model_auth_options.go` версии
PocketBase из `go.mod`.
- Комментарии шага и `provider.go` говорят об умолчании одно и то же. Оракул —
чтение обоих мест подряд.
- Гейт зелёный целиком. Оракул — `task gate`.
## Рамки
Правка применённого шага схемы — необратимое: решение принимает человек. Новым
шагом это не лечится: новый шаг накатывается вперёд, а исправить нужно ветку
отката прежнего.
-54
View File
@@ -1,54 +0,0 @@
# 🐞 Вести учёт употреблённых состояний входа на сервере
- **Тип:** fix
- **Категория:** Очередь — Тот же файл и та же спека, что у строки выше.
- **Зачем:** Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены.
Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа —
[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md),
остаток пункта 4.
Носитель состояния здесь — кука, которую сервис ставит на время входа: в ней
лежат выданное состояние и проверочный код, и по ней сверяется возврат.
Спека требует, чтобы состояние было одноразовым: возврат с уже употреблённым
отвергается наравне с невыданным. Сегодня это выполняется тем, что носитель
состояния убирается у браузера на возврате — и для обычного человека этого
достаточно: второй раз тот же адрес возврата сверку не пройдёт.
Чего это не закрывает: тот, кто носитель контролирует, поставит его себе заново
и повторит возврат. Отказ тогда наступит только потому, что код у провайдера
одноразовый, — то есть гарантия перенесена на внешнюю систему, чего норма не
допускает.
Цена сегодняшнего состояния невелика, поэтому задача и отложена: код живёт
минуты, а вход у провайдера всё равно нужен. Цена решения — своё хранение
состояний со сроком жизни и его чистка.
## Воспроизведение
1. Пройти вход до конца: получить сессию по возврату от провайдера.
2. Поставить носитель состояния заново — тем же значением, которое сервис выдавал
на первом шаге.
3. Повторить тот же запрос возврата.
4. Сверка состояния проходит, и запрос уходит в обмен. Отказ наступает только
потому, что код у провайдера одноразовый, — то есть одноразовость держит
внешняя система, а не сервис.
## Затрагивает
- обработчики начала входа и возврата;
- место хранения употреблённых состояний: своя коллекция хранилища либо память
процесса — выбор входит в задачу;
- дельта-спека `access`, требование «Вход через внешнего провайдера» — сценарий
«Возврат нельзя переиграть» получает настоящий оракул.
## Критерии приёмки
- Повторный возврат с тем же состоянием отвергается, даже если носитель
восстановлен вручную. Оракул — тест: пройти вход, затем повторить тот же
возврат с заново поставленным носителем; сессия не открывается.
- Состояния не копятся без предела. Оракул — тест либо чтение кода: у записи
состояния есть срок жизни, и просроченные убираются.
- Вход по-прежнему проходит целиком. Оракул — существующий тест входа через
подставного провайдера остаётся зелёным.
+72
View File
@@ -0,0 +1,72 @@
# ✨ Пускать по доверенным заголовкам Authelia вместо входа OIDC
- **Тип:** feature
- **Категория:** Очередь — Вход переезжает на заголовки прокси: пять задач про механику OIDC этим закрываются, а всё, что трогает вход и локальный прогон, ставится на него
- **Зачем:** Вход собран своими руками — состояние, PKCE, обмен кода внутрипроцессным запросом к роутеру хранилища, — а обратный прокси уже отдаёт трём соседним сервисам заголовки Remote-* от Authelia; заодно секрет клиента лежит в базе, а панель обходится подменой знака в пути.
Кто пришёл, сервис узнаёт из заголовка, который поставил обратный прокси, сходив
к Authelia. Своего входа у сервиса не остаётся вовсе: ни адреса, уводящего к
провайдеру, ни возврата, ни куки сессии, ни выхода.
Контур к этому готов: `files/caddyproxy/Caddyfile.template` в
`pet-project-server` уже отдаёт `Remote-User`, `Remote-Groups`, `Remote-Email` и
`Remote-Name` панели, netdata и remembos, а правила для этого сервиса там нет
вовсе — он не выложен. Контейнер портов наружу не публикует.
Три следствия, ради которых это и делается. Отзыв доступа перестаёт ждать
семи суток: Authelia судит каждый запрос, а не только вход. Секрет клиента
уходит из конфига и из базы — вместе с ним снимается изъятие из инварианта
«Секрет не покидает конфиг». Панель закрывается доменом, а не правилом на литерал
пути, и обход `/%5f/` перестаёт существовать.
## Затрагивает
- адреса `GET /auth/login`, `GET /auth/callback`, `POST /auth/logout` — исчезают
целиком вместе с корнем `/auth`;
- куки `transcriber_session` и `transcriber_login`, слои `SessionFromCookie` и
`BlockSessionRefresh`;
- заголовки `Remote-User`, `Remote-Email`, `Remote-Name` — новый недоверенный
вход, и адрес источника запроса как условие доверия к ним;
- секция `[auth]` конфига и `config.example.toml`: имена ключей — необратимое;
- настройки провайдера у коллекции `users` (`ApplyProviderSettings`) и новый шаг
схемы; применённый `202608120001_oidc_login` не переписывается;
- собственные адреса входа хранилища под `/api/collections/users/`: обмена кода,
входа по паролю и запроса подтверждения почты больше не предъявляет никто;
- `cmd/oidcstub` и подставной провайдер в тестах входа;
- способ представиться на машине без прокси: сегодня им служит заглушка
провайдера, и замену ей называет эта задача — на неё опирается `dev-run-task`;
- спека `access`; `docs/security.md` — периметр, недоверенный вход, «Четвёртый
сдвиг»; `CLAUDE.md` — изъятие из инварианта о секрете; `docs/architecture.md`;
- правило прокси и правило Authelia для домена сервиса — они живут в
`pet-project-server`, вне этого репозитория.
## Критерии приёмки
- Обращение к адресу приложения с заголовком от доверенного источника идёт от
имени учётной записи, заведённой при первом таком обращении, а повторное с тем
же значением попадает в ту же запись. Оракул — тест обработчика: два запроса
подряд, в хранилище одна запись пользователя.
- Тот же заголовок с недоверенного адреса даёт `401`, а не вход под названным
именем. Оракул — тест: запрос с адресом источника вне перечня доверенных.
- Собственные адреса входа хранилища сессии не выдают и учётную запись не
меняют. Оракул — тест по перечню адресов под `/api/collections/users/`: каждый
отвечает отказом.
- Механики OIDC в дереве не осталось: корня `/auth`, кук входа,
`ApplyProviderSettings`, `cmd/oidcstub` и секрета клиента в конфиге. Оракул —
поиск по этим именам плюс зелёный `task gate`.
- Разграничение записей по владельцу работает как прежде: чужая запись
неотличима от несуществующей. Оракул — существующие тесты владельца остаются
зелёными.
## Рамки
Ключ учётной записи — `Remote-User`: переименование пользователя в Authelia
заведёт новую запись, и прежние записи останутся у прежней. Это принятая цена,
и она записывается в спеку, а не обходится.
Второй уровень доступа по `Remote-Groups` здесь не заводится: его потребителя —
страницы расхода — ещё нет.
Половина работы живёт в `pet-project-server` и этим репозиторием не проверяется
ничем: задача обязана назвать требование к контуру в `docs/security.md`, а
выкладку запускает человек.