- адреса приложения переехали в своё пространство `/app/`, опрос готовности убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи, текст — отдельным адресом названного вида - заведена единая точка отображения доменной ошибки и слой, приводящий к той же форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением - у записи появились имя файла отправителя, длительность и размер своими колонками, а у ленты владельца — свой индекс: без него страница сканировала весь архив сервиса
10 KiB
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 адреса почты в ответе нет