From e4441f3c49c7c4c2c67393a2a7e53ec290281e80 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 22 Aug 2026 17:43:18 +0300 Subject: [PATCH] =?UTF-8?q?tasks:=20=D0=B2=D1=85=D0=BE=D0=B4=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B5=D0=B7=D0=B6=D0=B0=D0=B5=D1=82=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=BA=D1=81=D0=B8,=20=D0=BF=D1=8F=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=20=D0=BF=D1=80=D0=BE=20?= =?UTF-8?q?OIDC=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - заведена `trusted-header-login` и поставлена в голову очереди - пять задач про механику OIDC закрыты как отменённые ею - `dev-run-task` и `api-tokens` переписаны под новый вход --- tasks/BACKLOG.md | 10 +-- tasks/REJECTED.md | 5 ++ tasks/items/api-tokens.md | 14 +++- tasks/items/dev-run-task.md | 40 ++++++----- tasks/items/login-surface-hypotheses.md | 44 ------------ .../login-url-from-collection-settings.md | 38 ---------- .../items/provider-code-out-of-storage-log.md | 44 ------------ ...ollback-restores-wrong-session-duration.md | 53 -------------- tasks/items/server-side-login-state.md | 54 -------------- tasks/items/trusted-header-login.md | 72 +++++++++++++++++++ 10 files changed, 113 insertions(+), 261 deletions(-) delete mode 100644 tasks/items/login-surface-hypotheses.md delete mode 100644 tasks/items/login-url-from-collection-settings.md delete mode 100644 tasks/items/provider-code-out-of-storage-log.md delete mode 100644 tasks/items/rollback-restores-wrong-session-duration.md delete mode 100644 tasks/items/server-side-login-state.md create mode 100644 tasks/items/trusted-header-login.md diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index 2f516de..a43ba11 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -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 нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. diff --git a/tasks/REJECTED.md b/tasks/REJECTED.md index b1c8a9c..17c0886 100644 --- a/tasks/REJECTED.md +++ b/tasks/REJECTED.md @@ -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 на каждом запросе. Шаг остаётся применённым и не переписывается, откаты по-прежнему ничем не подключены. Была секция: Очередь. diff --git a/tasks/items/api-tokens.md b/tasks/items/api-tokens.md index 6a5b521..c0223a4 100644 --- a/tasks/items/api-tokens.md +++ b/tasks/items/api-tokens.md @@ -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`. diff --git a/tasks/items/dev-run-task.md b/tasks/items/dev-run-task.md index 4494f76..c053442 100644 --- a/tasks/items/dev-run-task.md +++ b/tasks/items/dev-run-task.md @@ -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` «Пароль владельца от панели не лежит в конфигурации» остаётся в силе, и работа его не трогает. Распознавание при выдуманных ключах не работает — это остаётся как есть, diff --git a/tasks/items/login-surface-hypotheses.md b/tasks/items/login-surface-hypotheses.md deleted file mode 100644 index 0e32fce..0000000 --- a/tasks/items/login-surface-hypotheses.md +++ /dev/null @@ -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`; -четвёртый воспроизводится своим прогоном без внешних систем и потому берётся -первым. Прогонов на боевом контуре не делаем. diff --git a/tasks/items/login-url-from-collection-settings.md b/tasks/items/login-url-from-collection-settings.md deleted file mode 100644 index c96ffc0..0000000 --- a/tasks/items/login-url-from-collection-settings.md +++ /dev/null @@ -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`, требование «Вход через внешнего провайдера» — состав - запрашиваемых сведений и способ проверочного кода перестают быть нашими. - -## Критерии приёмки - -- Адрес согласия строится из настроек коллекции: правка провайдера в панели - меняет адрес, куда уводит вход. Оракул — тест: сменить настройки провайдера в - хранилище и убедиться, что адрес перенаправления изменился. -- Проверочный код и состав запрашиваемых сведений берутся у библиотеки, а не - записаны у нас. Оракул — чтение кода: своих литералов состава больше нет. -- Вход по-прежнему проходит целиком. Оракул — существующий тест входа через - подставного провайдера остаётся зелёным. diff --git a/tasks/items/provider-code-out-of-storage-log.md b/tasks/items/provider-code-out-of-storage-log.md deleted file mode 100644 index e6ee2cc..0000000 --- a/tasks/items/provider-code-out-of-storage-log.md +++ /dev/null @@ -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`, перечень мест, где оседает чувствительное. - -## Критерии приёмки - -- После входа код провайдера не встречается в журнале запросов хранилища. - Оракул — тест: пройти вход подставным провайдером, затем отобрать записи - журнала и убедиться, что значения кода в них нет. -- Строка о запросе к адресу возврата в журнале остаётся: пропажа самого следа - не годится, прослеживаемость входа нужна. Оракул — тот же тест: запись о - запросе есть, кода в ней нет. -- Норма и код сошлись: либо спека выполняется буквально, либо в ней названо - изъятие с ценой. Оракул — чтение требования против исхода первого теста. diff --git a/tasks/items/rollback-restores-wrong-session-duration.md b/tasks/items/rollback-restores-wrong-session-duration.md deleted file mode 100644 index 5cd5cb0..0000000 --- a/tasks/items/rollback-restores-wrong-session-duration.md +++ /dev/null @@ -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`. - -## Рамки - -Правка применённого шага схемы — необратимое: решение принимает человек. Новым -шагом это не лечится: новый шаг накатывается вперёд, а исправить нужно ветку -отката прежнего. diff --git a/tasks/items/server-side-login-state.md b/tasks/items/server-side-login-state.md deleted file mode 100644 index 900f442..0000000 --- a/tasks/items/server-side-login-state.md +++ /dev/null @@ -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`, требование «Вход через внешнего провайдера» — сценарий - «Возврат нельзя переиграть» получает настоящий оракул. - -## Критерии приёмки - -- Повторный возврат с тем же состоянием отвергается, даже если носитель - восстановлен вручную. Оракул — тест: пройти вход, затем повторить тот же - возврат с заново поставленным носителем; сессия не открывается. -- Состояния не копятся без предела. Оракул — тест либо чтение кода: у записи - состояния есть срок жизни, и просроченные убираются. -- Вход по-прежнему проходит целиком. Оракул — существующий тест входа через - подставного провайдера остаётся зелёным. diff --git a/tasks/items/trusted-header-login.md b/tasks/items/trusted-header-login.md new file mode 100644 index 0000000..f2c8ef1 --- /dev/null +++ b/tasks/items/trusted-header-login.md @@ -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`, а +выкладку запускает человек.