Модели разработки на примере интеграции OpenID Connect

c468f6f98a2155d8a2542627fdb46b6b.png

Привет!

Меня зовут Алексей Дёрин, и я являюсь ведущим разработчиком в КРОК Облачные сервисы. 

Не так давно в нашем Облаке КРОК мы зарелизили одну небольшую фичу — возможность авторизации через OpenID Connect. На примере её разработки я хочу показать, как процесс создания и доведения до конечного результата ложится на различные существующие практики разработки.

Речь пойдет об итеративном и инкрементальном подходах разработки, об Agile, пресловутом «водопаде» и о том, как это выглядит на практике и какого результата можно добиться.

Мне очень нравится одна статья, описывающая методы разработки в координатах итерационного и инкрементального подходов, и видео от David Michel, где он очень наглядно показывает через термин Fidelity разницу между этими подходами.

Подходы к разработке by Henny Portman

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

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

Наиболее эффективным является совмещение этих двух подходов:  релизить по фичам,  причём в усечённом виде, наращивая каждый цикл, их проработанность или количество. Такой подход и понимается под Agile.

Постановка задачи

Большинство задач на разработку при первичной постановке выглядят как «сделайте хорошо, чтобы работало». Т.е. она зачастую весьма размыта и очень поверхностна. У вас даже может не сложиться понимания, что именно надо сделать. Какая полнота функциональности нужна, как именно надо это реализовать, как это будет накладываться на пользовательский опыт с уже существующей функциональностью?  При этом от вас сразу попросят сроки готовности, и желательно не очень длинные. 

Классический подход («водопад») зарелизить всё и сразу тут не подойдет, нужно делить на этапы и идти по ним. Важно сразу заскоупиться на создании только MVP, т.е. минимально жизнеспособного продукта. MVP позволит с одной стороны показать нечто готовое, а с другой — понять направление дальнейшего развития и увеличения полноты функциональности. На его основе можно собрать отзывы и доработать его уже до MMP, т.е. продукта, удовлетворяющего минимальные потребности пользователей.

Наш бизнес-юнит КРОК Облачные сервисы имеет в своём портфеле решений как публичное Облако КРОК, так и частные облака. Для каждого продукта у нас свой ответственный по продажам со своими самыми важными хотелками.

Как-то раз приходит один из них, Серёга, и говорит, что неплохо бы научить облака работать не только c собственными облачными пользователями, но и с пользователями уже готовой инфраструктуры, например Active Directory. Потому что у частных облаков, например, это весьма популярная история и дублировать десятки, а то и сотни пользователей — та ещё задача. А что у публичного облака? А публичное — это не его головная боль, сделайте, пожалуйста. Предварительное обсуждение с продукт овнером публичного облака резко усложнило анализ. Он хотел, чтобы функциональность была полезна и для его пользователей.

Становится понятно, что у нас должен быть универсальный сервис, и возможное решение может не подойти для всех. Поэтому нужно сосредоточиться только на разработке MVP, т.к. нам непонятен конечный результат и полный объём доработок. И только после MVP можно будет сделать уже какой-то MMP для пользователей. При этом первоначально надо накидать некий POC (proof of concept), чтобы понять правильность выбранного пути и подтвердить ожидания от сервиса.

Таким образом уже сразу вырисовывается итерационный путь разработки в виде POC → MVP → MMP. Кроме того, закрадываются подозрения, что целевая функциональность может быть разбита на ряд фичей поменьше. Их можно будет релизить по отдельности, открывая возможность инкрементального пути, но это, как минимум, после анализа.

Анализ

Это был, пожалуй, самый сложный этап. На нём надо было определить что мы делаем, как мы делаем и подойти к реализации POC-решения.

Так как в изначальной постановке прозвучала необходимость интеграции с Active Directory (AD) от Microsoft, то на нём и сосредоточились. AD есть в большинстве корпоративных инфраструктур, к нему можно подключиться и что-то пилотное запилить. Это либо протокол LDAP, либо же Kerberos с глубокой интеграцией в инфраструктуру от Microsoft. Для публичного облака выглядит, мягко говоря, так себе, даже LDAP over SSL (LDAPS).

Главная причина в том, что облачная платформа становится ответственной за работу с паролями пользователей, которые она пропускает через себя. Да, можно их не хранить, но обязательно нужно за ними следить и нести ответственность. Например, их можно случайно залогировать. И самое главное, как доказать пользователям, безопасникам и различным аудиторам, что мы ничего не храним, хотя имеем все возможности. Следовательно нужно посмотреть в сторону внешней аутентификации и авторизации.

Я провёл анализ всех существующих решений других облаков. Перенимать передовой опыт гораздо лучше, чем строить велосипеды и топтаться по старым граблям.

Наиболее популярные варианты — это SAML и OpenID Connect (OIDC). При этом SAML, древний как мамонт, существует ещё со времен популярности SOAP и XML-протоколов интеграции. OIDC посвежее, работает на JSON-объектах. Хотя он уже тоже не молод, но всё ещё остаётся менее известным и многими воспринимается как вариация SAML.

В процессе анализа способа подключения к Active Directory я постоянно консультировался с продукт овнерами, тимлидами, а также с самими пользователями. Сводил воедино их требования и предположения насчёт того, как эта функциональность должна работать. Пришлось даже столкнуться с малой известностью деталей работы OIDC и богатым опытом работы с SAML. Одно из главных требований сводилось к необходимости синхронизировать состояние пользователей (их права и блокировку аккаунта) с первоначальным источником. Но SAML реализует только одноразовое получение данных о пользователе в момент логина. Если же с пользователем что-то произойдёт в AD после авторизации, облако ничего об этом не узнает до следующего логина. А заставлять пользователя слишком часто перелогиниваться недопустимо.

Чаша весов начала склоняться в сторону LDAPS. Решение старое, понятное, часто применяемое в различных программных продуктах. Но у меня оставался ещё вариант с OIDC. Он не просто посвежее чем SAML, но и гораздо функциональнее, так как построен поверх OAuth2. В нём есть механизм скрытой повторной авторизации с использованием refresh_token. Он позволяет создать подобие периодической синхронизации, что и стало палочкой-выручалочкой. Скептиков удалось переубедить, показав работу POC.


Забегая вперёд могу сказать, что выбор OIDC вместо LDAPS оказался верным решением. Оно позволило наращивать функциональность и экономить силы и время, но об этом позже.

Оставался вопрос с правами: как именно выдавать права пользователям?

Опыт работы с AD, где система прав в основном основана на группах, помог нам ответить на него. При работе с облаком, мы применяем ту же концепцию, выдавая права на основе AD-групп и определяя права конкретного пользователя на основе его принадлежности к этим группам. Через OIDC нельзя получить список всех имеющихся групп в AD. Можно получить только группы у залогинившегося пользователя.

Я исходил из того, что для разделения прав в AD будут созданы отдельные группы облачных пользователей и ожидал, что таких групп будет немного (до 20). Поэтому, вместе с коллегами мы нашли компромиссное решение: предварительно для каждой такой группы в облаке нужно вручную создавать запись-проекцию и выдавать ей определённый набор прав. В этом случае после логина на основе принадлежности к указанным AD-группам пользователь будет получать заданные права.

Концепт

Формально POC мог бы пройти по всем этапам: анализ → разработка → тестирование → внедрение. Но функциональность POC была достаточно мала, поэтому он был сделан на скриптах,  лишь для демонстрации работы OIDC в целом и «синхры» в частности. Поэтому этапов как таковых, кроме разработки, у него не было. Кроме того, фактически он был сделан в рамках этапа проектирования (в терминах «водопада»), чтобы была возможность оценить объём требуемых работ для MVP и заложить их в план.

Так как мы всё ещё ориентировались на решения от Microsoft,  был взят ADFS, который с версии 2016 полностью поддерживает OIDC.

В Облаке КРОК был поднят Windows Server 2016, настроен AD и ADFS. И прямо на скриптах на самом этом сервере был показан рабочий вариант данного механизма.

Заходим в браузере на эндпоинт авторизации:

https://localhost/adfs/oauth2/authorize?client_id=test&response_type=code&redirect_uri=https%3A%2F%2Fcloudserver.login.tt&scope=openid&response_mode=fragment&nonce=1

ADFS отрисовывает форму авторизации, вводим логин пароль доменной учётки, нас редиректит на переданный в запросе выше адрес redirect_uri

https://cloudserver.login.tt#code=XXXXX

С этим кодом отправляем скриптом обратно post-запрос на получение токенов.

   client_id = "test"

   redirect_uri = "https://cloudserver.login.tt"

   grant_type = "authorization_code"

   code = "XXXX"

}

Invoke-RestMethod https://localhost/adfs/oauth2/token -Method Post -Body $Body -OutFile response.txt

В результате в response.txt находим JSON с тремя токенами:  id_token, access_token и refresh_token.

Далее с определённым периодом можно запрашивать токены с использованием refresh_token.

Далее с определённым периодом можно запрашивать токены с использованием refresh_token.

$Body = @{

   client_id = "test"

   refresh_token = "YYYYY"

   grant_type = "refresh_token"

}

Invoke-RestMethod https://localhost/adfs/oauth2/token -Method Post -Body $Body -OutFile response.txt

И снова в response.txt можно найти все три токена.

Если пользователя добавили или удалили из AD-группы, то это изменение будет отражено в новом присланном id_token. Если же его заблокировали, то ADFS отклонит запрос и пользователя нужно блокировать как неактивного.

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

Это приводит нас к пониманию, что внешние пользователи — непостоянные и могут быть заблокированы в любой момент без нашего ведома и контроля. Соответственно, нам нужно рассматривать таких пользователей как временных и рекомендовать не использовать их для задач автоматизации.

MVP

POC готов и защищён перед тимлидами. Определён скоуп задач, они все заведены в Jira, описана итоговая функциональность для приёмки, даны все оценки, они заложены в план. Осталось только добавить код в платформу, отрефакторить IAM для поддержки нового типа пользователей, добавить новые сущности в модель, перенести логику из POC-скриптов, написать для всего этого UI и так далее.

Снова возникает желание пройти это не за один релизный цикл, а за несколько,  например,  крупный рефакторинг выпустить отдельно. Кроме того можно разделить связку бэк-фронт, используя вертикальный подход.  Сначала релизится API бэка на стабах, который ничего не делает, а лишь возвращает заглушки данных. Затем параллельно идёт проработка бэка и разработка UI. Таким образом, мы позволяем команде бэка и команде UI работать одновременно, не ждать друг друга и релизить свои задачи.

Важно отметить, что описанные микрорелизы являются unstable-релизами и в конечном счёте сводятся к одному stable-релизу. Это позволяет им быть отдельными этапными задачами в Jira, а тимлидам и менеджерам — контролировать прогресс работы над задачей.

План выполнен (даже с некоторым опережением), MVP готов. Его функциональность собрана в релизную версию, но скрыта от всеобщего доступа. Выдаём её либо пользователям, которые запрашивали её в прошлом, либо тем, с кем продавцы частных облаков договорились о закрытом тестировании. Некоторые из них не только подключились, проверили, посмотрели, но даже написали отзыв.

Снова приходит уже упомянутый Серёга и тыкает пальцем в окно —, а там санкции, импортозамещение, отказ от инфраструктуры Microsoft, миграции на сторонние LDAP-решения и вот это всё. Где он был на этапе анализа? Наверное был занят важными делами. 

Хорошо, что мы пошли по итерационному пути, сделали лишь MVP, потратили на это не так много сил, а сейчас как раз собираем отзывы. Его новый запрос фактически стал одним из таких отзывов, из которых мы формируем новый объём задач.

Keycloak

Тем не менее, указанные события имеют место быть и надо что-то с этим делать.
Одним из популярных IAM-решений является Keycloak. Мощная IAM-платформа, которая может сама быть хранилищем пользователей и групп, или может подключаться через LDAP к другим хранилищам и выгружать пользователей к себе. При этом она поддерживает множество разных протоколов для интеграции,  в том числе OIDC, который мы верно выбрали для нашего решения на этапе анализа.

Протоколы взаимодействия с Keycloak

Протоколы взаимодействия с Keycloak

Таким образом, будучи развернутым на стороне пользователя, Keycloak может послужить промежуточным звеном для интеграции LDAP-хранилища и облачной платформы.

В связи с этим, возникла необходимость в небольших изменениях, так как Keycloak использует немного отличающиеся от ADFS адреса эндпоинтов. Мы решили перейти на отдельные ссылки для каждого используемого эндпоинта вместо одной ссылки на сервер OIDC-провайдера.

Кроме того, пользователи справедливо указали на недостаток безопасности в нашей реализации. Мы не использовали механизм client secret при запросе токенов и не использовали подпись сертификатами. В ответ на обратную связь, мы по полному циклу внесли изменения и написали документацию. И уже «пропатченная» версия MVP была представлена пользователям с целью повторного сбора обратной связи.

Azure

Для частных облаков сделанное решение вполне подходило. В публичном же кроме редких тестовых подключений ничего не происходило до того момента, пока один из пользователей настоятельно не захотел авторизоваться через Azure AD. Как бы странно это ни звучало — на фоне санкций. Когда-то у него был ADFS-сервер, но уже пару лет как его вывели из эксплуатации, переехав в Azure AD.

Microsoft Identity Platform может использоваться в качестве провайдера внешних удостоверений. Я был уверен, что всё сработает. OIDC-стандарт воспроизвели, два различных продукта без особых усилий подключили. Тем более один из них — ADFS, — от того же вендора Microsoft. Но внезапно обнаружилось, что Azure не присылает в id_tokenникаких данных по пользователю, кроме групп.

Я и команда тесно взаимодействовали с пользователем. Все настройки на стороне Azure были сделаны верно, а данных не было. Значит что-то нужно было сделать на нашей стороне.

Помимо id_token Azure не присылало и refresh_token, без которого не работает та самая «синхра». Пользователя пришлось бы блокировать по времени жизни access_token, т.е. по-умолчанию через час. Откровенно говоря, он и так не мог авторизоваться из-за отсутствия других данных, поэтому блокировка была бы второстепенной проблемой. Тем не менее, именно поиск по отправке refresh_tokenпривел нас в документацию Azure AD, в которой Microsoft описывает optional-поле scope для запросов к token-эндпоинту.

Выдержка из документации Azure AD с форматом запроса к token-эндпоинту

Выдержка из документации Azure AD с форматом запроса к token-эндпоинту

Делаем вывод:  чтобы scope был передан от Azure AD, его нужно не только разрешить в Azure, но ещё и запросить клиенту при получении токена. В очередной раз можно попенять Microsoft за вольное отношение к стандартам и констатировать, что переезд ADFS → Azure AD без правок на стороне клиента невозможен.

Ещё один коротенький, но тем не менее полный релизный цикл от анализа до внедрения.

MMP

На текущий момент наше решение выглядит минимально законченным: у него есть пользователи во всех типах инсталляций — как в публичной, так и в приватных; одни через Azure AD, другие через Keycloak. Путь разработки занял несколько отдельных релизных циклов и множество микрорелизных.

Можно ли было его пройти сразу, за один проход? Конечно!  Но надо было заранее знать, какие будут проблемы, и что в итоге мы должны получить, т.е. сразу знать, что решение через OIDC всех устроит, сразу целиться на упомянутые две системы, сразу подтягивать компетенции по работе с ними. А если не знать с самого начала, то единоразовая итерация была бы долгой, затяжной. Вместо фидбэков от пользователей были бы длинные циклы анализа в поисках тех или иных возможностей по интеграции. А главное, росли бы риски ошибиться и слить свой труд и время в пустоту. Вышло бы дороже и нервознее и не факт, что получилось бы хорошо.

Поэтому поэтапная работа помогает всем участвующим в процессе: одним — планировать и отчитываться, другим — спокойно работать и видеть результат своего труда в виде благодарных отзывов.

Что касается подходов, итерационного и инкрементального, то получилось и то, и другое. Итерационно — от сырого POC к готовому MVP, и инкрементально — от устаревшего ADFS к современному Azure AD и мейнстримному Keycloak.

Заключение

Придя в эту точку я обнаружил, что изначальная постановка «подключите Active Directory» в конечном счёте превратилась в подключение OIDC-провайдеров. При этом все сервисы, таблицы, внутренний API и даже документация сохранили следы AD-направленности в виде нейминга переменных и некоторых терминов.

Можно ли было это предусмотреть заранее? Можно, ведь на этапе анализа уже было выбрано OIDC-решение. Однако до стадии MMP было не до конца понятно его целевое использование, все разговоры вокруг него шли в разрезе LDAP-хранилищ и Active Directory. И само решение было весьма компромиссным для всех облаков.

В итоге, через несколько циклов разработки даже у относительно свежего решения появляется legacy, требующее глубокого рефакторинга кода. Но т.к. это вопрос всего лишь нейминга, скорее всего так оно и останется. Кроме того, название AD мне нравится по ряду личных причин.

© Habrahabr.ru