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