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/records-list-screen.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 объекта, из журнала снова собирается ссылка на чужую запись.
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в cmd/transcriber проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
- [🧹 Запретить обращаться к 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, и по ним читатель строит решения о выкладке и о периметре.
- [✨ Сопоставить пользователя 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) — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем.
- [🧹 Покрыть тестами разбор вывода ffprobe](items/metaviewer-adapter-tests.md) — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто.
- [🧹 Задать таймауты обращениям к внешним сервисам](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 `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-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
- **Категория:** Очередь — Второй способ представиться ставится на готовые владельца и контракт, иначе форма ошибки переписывается дважды.
- **Зачем:** Вход через OIDC закрывает API целиком, а скрипту браузерная сессия недоступна: автоматизировать загрузку станет нечем.
- **Зачем:** Скрипту недоступны ни браузерная сессия, ни вход у Authelia: домен целиком стоит за прокси, и автоматизировать загрузку нечем.
Запрос без токена не проходит, а скрипт ходит в API по токену, выпущенному
пользователем, и видит ровно его записи.
Половина работы при этом лежит на контуре: домен целиком стоит за `forward_auth`
Authelia, и запрос скрипта не доходит до сервиса вовсе — его отбивает прокси.
Значит адресам API нужно правило, пропускающее их к сервису, а сервис судит
токен сам. Правило живёт в `pet-project-server`, вне этого репозитория.
Токен принадлежит учётной записи и даёт ровно её права: записи, заведённые по
токену, видны владельцу в приложении, и наоборот.
## Затрагивает
- заголовок авторизации у всех адресов приложения `/app/`;
- заголовок авторизации у всех адресов приложения `/app/` и его отношение к
заголовку, который ставит прокси: два способа представиться и один владелец;
- правило Authelia, пропускающее адреса API мимо входа, — в `pet-project-server`;
- таблица токенов: владелец, имя, отпечаток, время выпуска и последнего
обращения, и её миграция;
- адреса выпуска, перечня и отзыва токена — `/app/me/tokens`;
@@ -35,6 +42,7 @@
Учётные записи по-прежнему заводит Authelia — свою регистрацию не делаем.
Сроков жизни и областей действия у токена не заводим: он даёт права владельца
целиком. Берётся после `oidc-login`: до неё представляться некому. Экрана выпуска здесь
целиком. Берётся после `trusted-header-login`: до неё представляться некому, а
она же решает, какому источнику сервис верит. Экрана выпуска здесь
нет — приложения ещё не существует, токен выпускается запросом к API; место
токена на экране настроек заводит `settings-screen`.
+22 -18
View File
@@ -1,18 +1,22 @@
# 🧹 Поднимать сервис и заглушку одной командой
# 🧹 Поднимать сервис для локальной работы одной командой
- **Тип:** chore
- **Категория:** Очередь — оснастка локального прогона: без неё каждую проверку начинают с двух терминалов и ручной уборки
- **Зачем:** Локальная проверка требует двух терминалов, ручной остановки обоих процессов и чистки каталога данных; учётную запись заводят проходом по браузеру, а владельца панели по одноразовой ссылке из журнала.
- **Зачем:** Локальная проверка требует ручной чистки каталога данных и остановки процесса, а владельца панели заводят по одноразовой ссылке из журнала.
Шаг `task dev CONFIG=<путь> USER=<почта>` поднимает сервис и подставной
провайдер OIDC разом, кладёт данные во временный каталог, заводит учётные записи
и гасит оба процесса по Ctrl+C.
Шаг `task dev CONFIG=<путь> USER=<имя>` поднимает сервис, кладёт данные во
временный каталог, заводит владельца панели и гасит процесс по Ctrl+C.
Работа состоит из двух частей, и они едут одним коммитом: инструмент
`cmd/devadmin` и шаг `dev`, который его зовёт. Порознь они не нужны — шаг без
инструмента не заведёт владельца панели, инструмент без шага останется вызовом,
который никто не делает.
Заглушки провайдера здесь больше нет: вход переезжает на доверенные заголовки
задачей `trusted-header-login`, и обычная учётная запись заводится первым же
запросом с заголовком. Каким способом заголовок попадает в запрос на машине без
прокси, решает та задача; этот шаг её решение только зовёт.
Задуманное устройство:
- конфиг пишет человек и передаёт путём — шаг его не сочиняет;
@@ -21,13 +25,12 @@
того, что написано в конфиге. Временное живёт в `/tmp` — в `data/` писать
запрещено (CLAUDE.md, «Запреты»);
- владельца панели заводит `cmd/devadmin` — отдельный main-пакет рядом с
`cmd/oidcstub`, в образ он не едет. Базу открывает тем же `pbrepo.New`, что и
`cmd/transcriber`, в образ он не едет. Базу открывает тем же `pbrepo.New`, что и
сервис, и потому получает её со схемой целиком; зовётся до подъёма сервиса, на
пустом каталоге. Пароль существующего владельца не переписывает;
- шаг заводит обычную учётную запись входом: поднимает `cmd/oidcstub` с
`-email` из `USER`, сам проходит цепочку входа и тем создаёт запись;
- оба процесса идут одной группой, `trap` на `INT` и `TERM` гасит их разом,
`wait` держит терминал.
- обычная учётная запись заводится первым запросом с заголовком: имя берётся из
`USER`, и заводить её отдельно шагу не нужно;
- `trap` на `INT` и `TERM` гасит процесс, `wait` держит терминал.
## Затрагивает
@@ -41,14 +44,14 @@
## Критерии приёмки
- Команда поднимает оба процесса и печатает адрес, по которому открывать
приложение. Оракул: прогон с тестовым конфигом — сервис и заглушка слушают свои
порты.
- Ctrl+C гасит обоих и не оставляет висящих процессов. Оракул: `pgrep` по обоим
именам после остановки — пусто.
- После запуска в базе есть учётная запись с переданной почтой, а владелец панели
входит своим паролем. Оракул: запрос к коллекции пользователей во временной базе
и вход по `auth-with-password` в коллекцию владельцев.
- Команда поднимает сервис и печатает адрес, по которому открывать приложение,
вместе со способом представиться. Оракул: прогон с тестовым конфигом — сервис
слушает свой порт, названный адрес отвечает.
- Ctrl+C гасит процесс и не оставляет висящих. Оракул: `pgrep` по имени после
остановки — пусто.
- Владелец панели входит своим паролем, а первый запрос с именем из `USER`
заводит учётную запись. Оракул: вход по `auth-with-password` в коллекцию
владельцев и запрос к коллекции пользователей во временной базе.
- Каталог данных лежит во временном месте, а `data/` не тронут. Оракул: путь базы
из журнала подъёма и `git status` после прогона.
- `task gate` остаётся зелёным, скрипт проходит `shellcheck`, а образ не получает
@@ -57,6 +60,7 @@
## Рамки
Шаг локальный: наружу не ходит, боевых ключей не требует и в образ не попадает.
Идёт после `trusted-header-login` — до неё локальный вход устроен иначе.
Пароль владельца в конфиг **не заводится** — требование спеки `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`, а
выкладку запускает человек.