Приключения Sign-In

Преамбула

Практически всем известна операция "Sign In", когда мы идентифицируемся на каком-нибудь сайте. Несмотря на кажущуюся простоту, у этого запроса непростая история и он может стать хорошим экспонатом для исследования подхода в дизайне приложений в экосистеме .NET.

Теории

Есть несколько теорий, которые интересно рассмотреть в контексте этого исследования:

  1. Теория категорий.
  2. Теория типов, унивалентная теория типов, гомотопическая теория типов (HoTT).
  3. Конструктивное программирование.
  4. Функциональное программирование, dependently typed programming (во многом как реальные практики использования теории типов, теории категорий и т.п.).

Участники

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 будут представлены в базе, предполагая, что мы не знаем, кто именно идентифицируется - пользователь или внешняя система. Не знаем как - логином и паролем или через социальную сеть и т.п.

Исследуя реализации очень важно анализировать, как сложно даются те или иные изменения. Они, по возможности, должны выглядеть итеративно. И добавлять новый функционал без стресса.

  1. F* – новый язык с зависимыми типами для .Net