## MODIFIED Requirements ### Requirement: Сессия предъявляется кукой Сервис SHALL принимать сессию, предъявленную кукой, — браузер отдаёт её сам, и своей страницы со скриптом для этого не требуется. Кука сессии MUST быть недоступна скриптам страницы (`HttpOnly`), MUST не уходить по незашифрованному соединению (`Secure`) и MUST не отправляться при переходе с чужого сайта (`SameSite=Lax` или строже). Имя куки нормативно — `transcriber_session`: смена имени молча выкидывает всех вошедших, а тест, ставящий и читающий одно и то же имя, этого не замечает. Хранилище читает предъявленную сессию заголовком `Authorization`, и этот способ остаётся рабочим: его требуют собственные адреса аутентификации хранилища. Сервис MUST перекладывать значение куки в этот заголовок **только когда заголовка нет**: предъявленный заголовок побеждает, иначе браузер с сессионной кукой получал бы не то, что предъявил на собственных адресах хранилища. Область действия слоя MUST быть ограничена **адресами приложения** — теми, что живут под его собственным корнем. Собственная поверхность хранилища под него не подпадает: часть её защищена сегодня ровно тем, что браузер заголовка сам не шлёт, и расширение слоя на всё сняло бы эту защиту молча. Область названа корнем, а не перечнем адресов: перечень рос бы с каждым новым адресом приложения, и забытый в нём адрес остался бы без слоя молча — сессия, предъявленная кукой, перестала бы на нём работать, а на соседнем работала бы. #### Scenario: Кука открывает доступ - **GIVEN** человек вошёл и получил куку сессии - **WHEN** он шлёт запрос к адресу приложения с этой кукой и без заголовка - **THEN** запрос проходит #### Scenario: Кука защищена от чтения скриптом - **WHEN** сервис ставит куку сессии - **THEN** она несёт признаки `HttpOnly`, `Secure` и `SameSite` #### Scenario: Предъявленный заголовок побеждает куку - **WHEN** запрос несёт и куку сессии, и заголовок `Authorization` - **THEN** проверку проходит значение заголовка, а не куки #### Scenario: Слой не расширяется на поверхность хранилища - **GIVEN** человек вошёл и получил куку сессии - **WHEN** он шлёт запрос к собственному адресу хранилища с одной лишь кукой - **THEN** значение куки в заголовок не перекладывается ### Requirement: У записи есть владелец, и чужую ей не отдают Сервис SHALL заводить у каждой принятой записи владельца — учётную запись, от имени которой запись принята, — и MUST отдавать данные такой записи только её владельцу. Запись без владельца MUST не заводиться ничем — ни приёмом, ни конвейером, ни рукой в панели: колонка владельца пустого значения не принимает, и норму эту держит capability `storage`. Владелец назначается один раз, при приёме, и MUST не меняться: совместного доступа, ролей и передачи записи другому сервис не знает. Владелец MUST браться из предъявленной сессии и ниоткуда больше. Владелец, пришедший полем запроса, дал бы всякому вошедшему право завести запись на чужое имя. Обращение к чужой записи MUST быть неотличимо от обращения к несуществующей. Отдельный отказ «доступ запрещён» превращает чтение в перебор — по разнице ответов считывается, какие записи заведены, а идентификатор записи и есть то, что разграничение прячет. Каким именно ответом это выражено, нормирует capability `archive`: там живут адреса чтения записи, и держатель нормы обязан быть один. Пустой владелец MUST не совпадать ни с одной записью. Правило записано со стороны **спрашивающего** и остаётся в силе, хотя записей без владельца в хранилище больше нет: спрашивающий с пустым владельцем — это вызов, у которого нет учётной записи, и отвечать ему надо отказом, а не выборкой. Держится оно отдельно от схемы намеренно: схема запрещает **заводить** ничью запись, а это правило запрещает **спрашивать** ничьим именем, и одно другое не заменяет. #### Scenario: Своя запись доступна - **GIVEN** человек вошёл и принял запись - **WHEN** он спрашивает карточку этой записи своей сессией - **THEN** ответ несёт данные записи #### Scenario: Чужая запись неотличима от несуществующей - **GIVEN** запись принята одним вошедшим - **WHEN** её карточку спрашивает другой вошедший - **THEN** ответ тот же, что и на неизвестный идентификатор, — и кодом, и телом #### Scenario: Владельца не задают запросом - **WHEN** запрос на приём записи несёт своё значение владельца - **THEN** владельцем принятой записи становится предъявитель сессии #### Scenario: Ничью запись завести нечем - **WHEN** запись пытаются завести с пустым владельцем - **THEN** хранилище её не сохраняет #### Scenario: Пустой владелец не открывает ничего - **GIVEN** заведены две записи: своя и чужая - **WHEN** карточку каждой спрашивают с пустым владельцем - **THEN** ответ на обе тот же, что и на неизвестный идентификатор ## ADDED Requirements ### Requirement: Приложение узнаёт вошедшего Сервис SHALL отдавать приложению сведения о том, кто вошёл, — `GET /app/me` — и MUST отвечать отказом `401`, когда сессии нет. Своей страницы со скриптом, которой сервер отрисовал бы имя вошедшего, у сервиса нет: приложение собирает разметку само и вошедшего узнаёт ответом. Кука сессии недоступна скриптам страницы, и прочитать из неё имя приложение не может вовсе — этот адрес единственный способ его узнать. Ответ MUST нести идентификатор учётной записи и имя, пригодное к показу, полями `id` и `name`. Адрес почты MUST в ответ не попадать: он приходит от провайдера и принадлежит человеку, а не сервису, и правило о непечатаемых значениях запрещает ему выходить наружу наравне с журналом. #### Scenario: Вошедший узнан - **GIVEN** человек вошёл и получил куку сессии - **WHEN** приложение спрашивает, кто вошёл - **THEN** ответ несёт идентификатор его учётной записи #### Scenario: Сессии нет - **WHEN** приложение спрашивает, кто вошёл, без сессии - **THEN** ответ имеет код `401` - **AND** тело ответа не несёт учётной записи #### Scenario: Адреса почты в ответе нет - **GIVEN** человек вошёл, и у его учётной записи есть адрес почты - **WHEN** приложение спрашивает, кто вошёл - **THEN** адреса почты в ответе нет