Приключения Sign-In
Преамбула
Практически всем известна операция "Sign In", когда мы идентифицируемся на каком-нибудь сайте. Несмотря на кажущуюся простоту, у этого запроса непростая история и он может стать хорошим экспонатом для исследования подхода в дизайне приложений в экосистеме .NET.
Теории
Есть несколько теорий, которые интересно рассмотреть в контексте этого исследования:
Участники
HTML-форма
Артефакт, генерируемый как статически, так и динамически. Может состоять из двух независимых блоков, отвечающих за ввод и отправку, а также за валидацию. Последнюю функциональность имеет смысл выделить, поскольку её реализация может отличаться. Например, сообщения об ошибках могут выводиться не непосредственно в форме, а в виде нотификаций.
Тесты
Проверка классическими функциями кэширования/мемоизации, журналирования, валидации. Когда мы модернизируем существующий процесс и в случае с кэшированием "заворачиваем" существующий функционал в кэширующий. В случае журналирования добавляем отдельную "ветку" процесса. А валидация становится чем-то вроде "вентиля" или скорее семафора.
TODO:Исследование с кэшированием интересно рассмотреть и на более приземлённом примере. Скажем, есть приложение, которое использует функцию факториала. Я хочу заменить это функцию факториала на Higher Order Functions мемоизации. У функциональных языков либо может быть предусмотрена такая возможность, либо они недооценивают и недопонимают DIP/service-container.
Workflow
Cases
Alerting. Были ситуаций, когда возникала необходимость анализировать параметры запроса и при его аномальных значениях формировать уведомление. При этом существующий workflow не менялся. Эта задача похожа на тонкий тюнинг журналирования. Не исключено, что он может и менять Workflow. Можно добавить соответствующий кейс для главного действующего лица (SignIn).
InBox
Исследовать railway-концепции и маршрутизацию в целом. В широком смысле этого слова (от транспортных артерий то цифровых сетей).
Эксплуатировать шаблон State. Просто его инстанцирование может быть более замысловатым, а сам он может участвовать в формировании класса запроса (или категорией-обобщением запроса).
Обычный бытовой task management часто требует выполнения рутинных операций. Например, отметить результат в collaboration tool (например, JIRA), внести в биллинг, удалить временные файлы и т.п. Подобные ситуации могут быть очень похожи с категориями Valid, Unsafe, Db и т.п. для SignIn. Будет интересно рассмотреть и их, может даже окажутся близкие аналогии.
Тестирование
Размышления об аспектном хранении объектов могут помочь в сэмплинге. То имя и описание тестируемого объекта, которое я использовал в некоторых проектах для интеграции с Gherkin явно претендуют на звание такого аспекта.
Скорее всего речь должна идти о более широком круге задач - спецификации. Объединяющем тестирование с ubiquitous language, документацией и служащей в целом для оценка качества итогового продукта. Спецификация - то, как видит конечный продукт пользователь, заказчик.
Sample-классы могли бы генерироваться полу-автоматически (некоторые из partial-части).
Aspect-oriented approach
Исследовать нечто похожее на columnar-storage, но применительно к дизайну аспектов (интерфейсов) класса-запроса. Вместо того, чтобы создавать композитный класс, их реализующий, можно хранить его аспекты отдельно (с кодом запроса). А workflow может определяться через населённость алгебраических классов. Например, некий обработчик может ожидать Valid+Authenticated. Интуитивно, очень интересно бы выглядели типичные конструкции CRUD-запросов, где львиная доля полей в части INSERT/UPDATE/SELECT пересекается. Для этой цели можно расширить немного судьбу SignIn-запроса механизмами добавления, обновления, блокировки пользователя.
За хранение аспектов может отвечать некоторый интерфейс IAspectProvider<T>, реализация которого в зависимости от аспектов может отличаться. Скажем, аспекты Lifetime могут храниться в виде массива структур, булевы аспекты как битовые карты и т.п. За основу в качестве кода запроса можно взять простой числовой счётчик. Тогда даже простой массив аспектов может легко адаптироваться под бинарный поиск.
Flow / Pipeline
- GET-запрос к /security/sign-in.
- Частью исследования может быть и ошибка 404.
- HTTPS-guard.
- Если запрос был осуществлён по протоколу HTTP, то перенаправить на тот же роут через HTTPS. На этом обработка запроса закончится.
- Генерация и возврат HTML-формы.
- Генерируем HTML-артефакт формы/страницы, который содержит login/password/rememeber-поля, каким-то образом сформированный контейнер для ошибок, а также надо обеспечить клиента информацией об ошибках, поскольку передаваться они будут в виде кода.
- Ошибки (валидации, идентификации, аутентификации) являются частью формы.
- Пользовательский ввод.
- Может выступать в роли некоторой абстракции, которую будет реализовывать Selenium-тесты.
- Кодирование формы в JSON на клиенте и отправка на "/api/v1/security/sign-in".
- HTTPS-guard.
- Если запрос был осуществлён по протоколу HTTP, то вернуть ошибку.
- Получение JSON запроса HTTP-сервером.
- Журналирование. Просто проходит через фильтр. Пишет в некий IEventLog.
- Http<SignInRequest> → Http<SignInRequest> сам в себя?
- Может быть несколько команд для журналирования в разные провайдеры, выполнять параллельно.
- Может быть полностью асинхронным.
- Аутентификация. Проверяет на IP, JWT. Расширяет/создаёт context. Потенциально генерирует ошибки 401/
- Может понадобиться доступ к хранилищу.
- Может быть несколько команд.
- Интеллектуальный load balancer.
- JSON декодирование. Потенциально генерирует ошибку 400. Может понадобиться доступ к настройкам (де)сериализации.
- Json<SignInRequest> → NotValidated<SignInRequest>
- Декодирование в JsonObject может вообще не иметь смысла, даже в SignIn-класс сомнительно. Можно интегрировать в парсер property-based валидаторы и отправить span-параметры сразу в SP.
- Валидация. Потенциально генерирует ошибку 422.
- Json<SignInRequest> → Valid<SignInRequest>
- Json<SignInRequest> → Invalid<SignInRequest>
- Может быть цепочка валидаций для различных интерфейсов. В том числе параллельных.
- Может быть какой-то дополнительный препроцессинг запроса (Preprocessed<SignInRequest>).
- Проставить дату запроса, чтобы проконтролировать хаммеринг. Pass-through.
- Подсолить Password. GDPR hashing. Pass-through.
- Теоретически все процессы, которые могут быть распараллелены и мы ожидаем Task.WaitAll.
- Alerting. Генерация дополнительной асинхронной ветви обработки с отправкой уведомлений.
- В процессинге запросов выглядит не очень, поскольку в нём ожидается выполнение всех ветвей, а alerting может быть полностью автономным. Плюс, ему может понадобиться уже сформированный SignInRequest.
- Отправка запроса в базу
- А если REST? И куда девать остальное "мясо" бизнес-команды?
- Автоматический роутинг в базу (Preprocessed<> → DbRequest<>) для определённых категорий запросов?
- Есть такой пользователь
- Ошибка базы (SomeDbException)
- Нет такого пользователя (UserNotFoundException)
- Хаммеринг (следующие попытки будут доступны через...) (UserHammeringException)
- Обработка ошибок (Exception → FormattedException<TException>)
- Журналирование
- Уведомления
- Редирект на сконфигурированную страницу для ошибки
- Форматирование и перевод ошибок
- Рендеринг JSON-ответа. По аналогии с декодированием можно сразу писать в HTTP Response с использованием некоторого JsonWriter для класса SignIn.
- Надо учесть, что JsonReader/JsonWriter работают с разными структурами. Если первый - (string login, string passwordm, bool Remember), то второй - с UserEntity, Exception.
- Отправка HTTP-ответа.
- Одним из продуктов может быть генерация Cookie. Причём, если в начальной форме была включена опция "Remember", то она повлияет на значение Expires куки.
- Если в изначальном запросе была опция ReferrerUrl, то мы можем перенаправить на указанную страницу (если она не относится к разряду системных) или на страницу по умолчанию в случае отсутствия.
- Декодирование JSON-ответа и модернизация DOM/State. Скажем, вывод информации об ошибке.
InBox
Одна из причин выбора платформы .NET заключается в том, что помимо гаммы языков, которые дают почти максимум возможностей для самых разносторонних экспериментов (C#, F#, F*), она также предлагает и мощные средства рефлексии, динамической компиляции и управляемой компиляции. Это очень важно, поскольку есть мнение, что не обязательно все возможности по валидации программы должны быть возложены на compile-time. И некоторые фазы, которые, скажем, Haskell реализует при весьма медлительной компиляции или возможности вроде dependent types, которые в самом C# отсутствуют, могут стать частью его runtime-ядра, который используя рефлексию и динамическую компиляцию может достраивать приложение на лету. По сути, классический переход compile → runtime может дополниться ещё одним этапом compile → meta-runtime → runtime. В качестве альтернативы может выступить управляемая компиляция, где рефлексию заменит анализ AST а динамическую компиляцию - кодогенерация.
Dependent-types программирование может быть реализовано в рантайме. Для workflow можно давать определение, что в этом направлении идут (в эту категорию попадают) те запросы, где login и пароль не пустой, что есть некоторый Valid<TRequest>.
Json<TRequest> может реализовывать парсинг именно заданного запроса TRequest. Скорее всего, сама категория JSON может исчезнуть и выродиться в морфизм. Вместо разбора JSON как универсальной структуры данных, создавая ещё одну сложную конструкцию в памяти, можно линейно читать входящий HTTP-поток и тут же формировать TRequest, без посредников и практически с встроенной валидацией поскольку по сути ориентируется на схему заданную TRequest. На примере SignIn он может прочитать первый символ и если квадратная скобка а не фигурная, то сразу выдать исключение. Если за фигурной скобкой не идёт имя "login" или "password", то также выдать исключение и т.п. Аналогичную ситуацию можно наблюдать при чтении из базы данных и записи в неё, когда (де)сериализация может производиться непосредственно из транспортного протокола базы данных и именно для заданного формата данных. Без использования дополнительных посредников вроде DataSet, SqlDataRecord, которые дублируют ещё и метаинформацию. Если попробовать подвести индуктивный итог рассуждению, то ни JSON, ни SqlDataRecord не являются продуктами, которые нас интересуют, что ставит под сомнение рациональность их существования.
Важно не забыть рассмотреть и то, как данные для SignIn будут представлены в базе, предполагая, что мы не знаем, кто именно идентифицируется - пользователь или внешняя система. Не знаем как - логином и паролем или через социальную сеть и т.п.
Исследуя реализации очень важно анализировать, как сложно даются те или иные изменения. Они, по возможности, должны выглядеть итеративно. И добавлять новый функционал без стресса.