Разработкой платформы мы занялись… случайно. Всё началось с пандемии, из-за которой правительство Люксембурга и распорядилось создать сервис онлайн-консультаций с врачами. Чтобы выжать из платформы максимум пользы, в неё решили добавить всё, что только нужно для полноценной работы врачей с пациентами. Обмен всевозможными медицинскими документами и даже выдача рецептов на лекарства исключениями не стали — старый вендор всё и сразу заложил в сервис. А затем, чтобы всё добро было в сохранности, к нам и обратились с уже готовым продуктом — нам всего-то и требовалось, что развернуть платформу в защищённой точке присутствия облака.
Через полтора месяца сервис успешно работал в люксембургском дата-центре EDH Tier IV, но ещё при первом знакомстве мы признали в нём слабое звено всего проекта: в отличие от точки присутствия, платформа защищённостью похвастаться не могла и обладала десятком других недостатков. Решение было очевидным — сервис нужно было делать с нуля. Оставалась убедить в этом правительство Люксембурга.
Чтобы предложить поставить на старой платформе жирную точку, нам достаточно было бегло осмотреть её. Вместо исправления всех возможных проблем безопасности и обновления дизайна сервис было проще сделать с нуля, и на то у нас были три веских довода.
Из-за этого проблем в платформе было немыслимое количество. Если бы для создания сервиса использовался какой-нибудь популярный фреймворк — Symfony, Laravel, Yii или что-то ещё, — то даже посредственные разработчики избежали бы большинства проблем безопасности, ведь ORM умеют подготавливать запросы к базе, шаблонизаторы — эндкодить полученный от пользователя контент, а формы по умолчанию защищены CSRF-токенами, да и авторизация и аутентификация, как правило, доступны практически из коробки. В этом же случае платформа заставила нашего разработчика ностальгировать по студенческим временам — код выглядел почти так же, как его первая лабораторная работа в университете.
Вот, например, как было реализовано подключение к базе данных. Учётные данные для подключения были захардкожены выше в том же файле.
После аудита мы поняли, что с такими недоработками выходить в production нельзя даже с простым блогом, не то что с платформой с конфиденциальными данными. Приведём некоторые из них.
Тут можно было бы продолжить и дальше, но уже этих проблем достаточно, чтобы понять: либо у системы были серьёзные проблемы, либо она сама была серьёзной проблемой.
Проблемами безопасности всё не ограничилось. К нашему удивлению, на проекте отсутствовала система контроля версий. Код был совершенно не структурирован. В root-директории web-сервера находились файлы вроде ajax-new.php, ajax2.php и все они использовались в коде. Отсутствовало и чёткое разграничение на слои (presentation, application, data). В подавляющем большинстве случаев файл с кодом представлял собой смешение PHP, HTML и JavaScript.
Всё это привело к тому, что когда нас попросили сделать примитивный backoffice для этой системы, лучшим решением стало развернуть рядом Symfony 4 в связке с Sonata Admin и вообще не касаться существующего кода. Понятно, что если бы нас попросили добавить новые возможности для врачей или пациентов, это отняло бы у нас кучу сил и времени. А поскольку об автоматических тестах речи не шло, вероятность сломать при этом что-нибудь была бы крайне велика.
Всего перечисленного правительству Люксембурга оказалось достаточно — нам дали зелёный свет на разработку новой платформы.
Готовиться к разработке новой платформы мы начали с самого начала — ещё когда увидели детище старого вендора. Поэтому, когда нам дали добро на разработку новой платформу, мы сразу приступили к созданию её MVP-версии. С этой задачей команда из четырёх PHP- и трёх фронтенд-разработчиков справилась за какие-то три с половиной недели. Все работы велись на Symfony 5, а делегировать удалось только видеозвонки и чаты — их реализовали с помощью нашего сервиса G-Core Meet. Пригодился и backoffice для старой системы: его удалось адаптировать к МVP всего за пару дней. В результате MVP-версия системы на 80% закрывала функционал старой платформы. Сейчас она, кстати, используется и для ещё одной задачи — в один момент мы клонировали MVP для хелпдеска агентства электронного здравоохранения Люксембурга, чтобы администраторы там могли созваниваться с пользователями.
Когда MVP был готов, мы приступили к разработке полноценной новой платформы. В качестве основы для API использовали API Platform и ReactJS в связке с Next.js для client-side. Без интересных задач не обошлось.
Одна из сложностей возникла с уведомлениями. Поскольку клиентами API могли быть как мобильный приложения, так и наши SPA, требовалось комбинированное решение.
Сначала мы выбрали Mercure Hub, с которым клиенты взаимодействуют через SSE (Server Sent Event). Но как бы это решение не пиарили сами создатели API Platform, наша мобильная команда его забраковала, так как с ним получать уведомления приложение могло только в активном состоянии.
Так мы пришли к Firebase, с помощью которого удалось добиться поддержки нативных пушей на мобильных устройствах, а для браузерных приложений оставили Mercure Hub. Теперь, когда в системе происходило какое-то событие, мы уведомляли пользователя об этом через нужный нам private-канал в Mercure Hub и, помимо этого, отправляли пуш ещё и на Firebase для мобильного устройства.
Почему мы сразу не реализовали всё на Firebase? Тут всё просто: несмотря на поддержку web-клиентов, браузеры без Push API — тот же Safari и большинство мобильных браузеров — с ним не работают. Однако, мы ещё планируем реализовать пуш-уведомления от Firebase для тех пользователей, которые используют поддерживаемые браузеры.
Другая интересная ситуация возникла, когда мы занимались функциональными тестами для API. Как известно, каждый из них должен работать в чистом окружении. Но каждый раз поднимать базу и заполнять в ней базовые fixtures + fixtures, необходимые для тестирования, оказалось накладно с точки зрения производительности. Чтобы этого избежать, мы решили на начальном этапе поднимать базу данных на основании маппинга сущностей (
Затем с помощью расширения dama/doctrine-test-bundle мы добились того, чтобы выполнение каждого теста оборачивалось в транзакцию и в конце тест-кейса откатывало её без фиксации. Благодаря этому, даже если в ходе тестирования в базу вносятся изменения, они не фиксируются, а база после прогона остаётся в том же состоянии, что и до запуска PHPUnit.
Чтобы на каждый эндпоинт был написан хотя бы один тест, мы сделали auto review тест. Он обнаруживает все зарегистрированные роуты и проверяет наличие тестов для них. Так, например, для роута app_appointment_create он проверяет, есть ли в папке
Дополнительно за качеством кода следят PHP-CS-Fixer, php-cpd и PHPStan c расширениями типа phpstan-strict-rules.
Для врачей и пациентов мы создали два независимых клиентских приложения с одинаковым UI и схожим возможностями. Чтобы наладить переиспользование функционала и UI в них, мы решили применить монорепозиторий, который включает в себя библиотеки и приложения. При этом сами приложения собираются и деплоятся независимо друг от друга, но могут зависеть от одних и тех же библиотек.
Такой подход позволяет в одном пул-реквесте создать фичу (библиотеку) и интегрировать её во все нужные приложения. Это преимущество и привело к использованию монорепозитория вместо реализации фич в отдельных npm-библиотеках и проектов в разных репозиториях.
Для конфигурации монорепозитория используется библиотека ns.js, которая с коробки позволяет собирать библиотеки и приложения React и Next.js, а именно этот стек и используется в проекте. Чтобы следить за качеством кода, мы используем ESLint и Prettier, для написания unit-тестов — Jest, а для тестирования компонентов React — React Testing Library.
Всего за пять месяцев все проблемы были разрешены, а новая платформа стала доступна пользователям любых устройств: мы подготовили веб-версию сервиса, а также мобильные приложения для iOS и Android.
Вот уже как 4 месяца сервис позволяет пациентам получать онлайн-консультации докторов и стоматологов. Они могут проходить как в аудио, так и видеоформате. В результате врачи выписывают рецепты и безопасно обмениваются медицинскими документами и результатами анализов с пациентами.
Платформа доступна для всех медицинских профессионалов, жителей и работников Люксембурга. Сейчас она работает в 2-х крупнейших учреждениях здравоохранения страны — в Больнице им. Роберта Шумана (Hopitaux Robert Schuman) и Больничном центре им. Эмиля Мэйриша (Centre Hospitalier Emile Mayrisch). Сервис развёрнут в защищённой точке присутствия облака G-Core Labs в люксембургском дата-центре EDH Tier IV, где для него настроена виртуальная среда в соответствии с нужными спецификациями.
Через полтора месяца сервис успешно работал в люксембургском дата-центре EDH Tier IV, но ещё при первом знакомстве мы признали в нём слабое звено всего проекта: в отличие от точки присутствия, платформа защищённостью похвастаться не могла и обладала десятком других недостатков. Решение было очевидным — сервис нужно было делать с нуля. Оставалась убедить в этом правительство Люксембурга.
Этот доктор сломался, несите нового: почему мы решили делать новую платформу
Чтобы предложить поставить на старой платформе жирную точку, нам достаточно было бегло осмотреть её. Вместо исправления всех возможных проблем безопасности и обновления дизайна сервис было проще сделать с нуля, и на то у нас были три веских довода.
1. Систему разрабатывали без фреймворка
Из-за этого проблем в платформе было немыслимое количество. Если бы для создания сервиса использовался какой-нибудь популярный фреймворк — Symfony, Laravel, Yii или что-то ещё, — то даже посредственные разработчики избежали бы большинства проблем безопасности, ведь ORM умеют подготавливать запросы к базе, шаблонизаторы — эндкодить полученный от пользователя контент, а формы по умолчанию защищены CSRF-токенами, да и авторизация и аутентификация, как правило, доступны практически из коробки. В этом же случае платформа заставила нашего разработчика ностальгировать по студенческим временам — код выглядел почти так же, как его первая лабораторная работа в университете.
Вот, например, как было реализовано подключение к базе данных. Учётные данные для подключения были захардкожены выше в том же файле.
if (!isset($db)) {
$db = new mysqli($db_info['host'], $db_info['user'], $db_info['pass'], $db_info['db']);
if ($db->connect_errno) {
die("Failed to connect to MySQL: " . $db->connect_errno);
}
if (!$db->set_charset("utf8")) {
die("Error loading character set utf8 for MySQL: " . $db->connect_errno);
}
$db->autocommit(false);
}
2. В платформе было множество проблем безопасности
После аудита мы поняли, что с такими недоработками выходить в production нельзя даже с простым блогом, не то что с платформой с конфиденциальными данными. Приведём некоторые из них.
- SQL-инъекции. В 90% запросов попадали введенные пользователями данные без их предварительной подготовки.
$sql = " UPDATE user SET firstname='%s', lastname='%s', born='%s', prefix='%s', phone='%s', country_res='%s', extra=%s WHERE id=%d ;"; $result = $db->query(sprintf($sql, $_POST['firstname'], $_POST['lastname'], $_POST['born'], $_POST['prefix'], $_POST['phone'], $_POST['country'], isset($_POST['extra']) ? "'".$_POST['extra']."'" : "NULL", $_SESSION['user']['id'] ));
- XSS-уязвимости. Пользовательский код никак не фильтровался перед выводом:
<button id="btn-doc-password" class="btn btn-primary btn-large pull-right" data-action="<?= $_GET['action'] ?>"><i class="fas fa-check"></i> <?= _e("Valider") ?></button>
Кроме того, попавшая в БД информация, вроде причины консультации с врачом, не фильтровалась ни перед записью в базу, ни перед рендерингом на странице. - Отсутствие проверки прав доступа. Достаточно было иметь аккаунт пациента и подобрать автоинкрементный ID другого пациента, чтобы без труда получить список приёмов и другой информации из профиля. Те же проблемы были и на стороне приложения доктора. Более того, зная ID врача не составляло проблем получить доступ к его приёмам.
- Отсутствие проверки загружаемых файлов. Через форму добавления документов можно было загрузить какой угодно файл в любую из папок, доступных для записи пользователям веб-сервера. Помимо того, поиграв с quеry-string запроса на загрузку документа можно было даже скачать файлы с исходным кодом.
$file_dir = $settings['documents']['dir'] . $_SESSION['client']['id'] . DIRECTORY_SEPARATOR . $_GET['id_user'];
- Устаревшие сторонние библиотеки. У старого вендора никто не следил за версиями сторонних библиотек, которые, кстати, вместо использования того же Composer просто копировались в проект. Более того, в некоторые из таких сторонних зависимостей вносились изменения «под себя».
- Небезопасное хранение паролей пользователей. Для хранения паролей использовались ненадёжные криптографические функции.
$sql = " SELECT id, firstname, lastname FROM user WHERE id=%d AND password=PASSWORD('%s') ;"; $result = $db->query(sprintf($sql, $_SESSION['user']['id'], $_POST['pass']));
- Уязвимость CSRF. Ни одна форма не была защищена CSRF-токеном.
- Отсутствие защиты от brute-force атак. Её просто не было. Никакой.
Тут можно было бы продолжить и дальше, но уже этих проблем достаточно, чтобы понять: либо у системы были серьёзные проблемы, либо она сама была серьёзной проблемой.
3. Код было сложно поддерживать и расширять
Проблемами безопасности всё не ограничилось. К нашему удивлению, на проекте отсутствовала система контроля версий. Код был совершенно не структурирован. В root-директории web-сервера находились файлы вроде ajax-new.php, ajax2.php и все они использовались в коде. Отсутствовало и чёткое разграничение на слои (presentation, application, data). В подавляющем большинстве случаев файл с кодом представлял собой смешение PHP, HTML и JavaScript.
Всё это привело к тому, что когда нас попросили сделать примитивный backoffice для этой системы, лучшим решением стало развернуть рядом Symfony 4 в связке с Sonata Admin и вообще не касаться существующего кода. Понятно, что если бы нас попросили добавить новые возможности для врачей или пациентов, это отняло бы у нас кучу сил и времени. А поскольку об автоматических тестах речи не шло, вероятность сломать при этом что-нибудь была бы крайне велика.
Всего перечисленного правительству Люксембурга оказалось достаточно — нам дали зелёный свет на разработку новой платформы.
Доктор едет-едет: как мы разрабатывали новую платформу
Готовиться к разработке новой платформы мы начали с самого начала — ещё когда увидели детище старого вендора. Поэтому, когда нам дали добро на разработку новой платформу, мы сразу приступили к созданию её MVP-версии. С этой задачей команда из четырёх PHP- и трёх фронтенд-разработчиков справилась за какие-то три с половиной недели. Все работы велись на Symfony 5, а делегировать удалось только видеозвонки и чаты — их реализовали с помощью нашего сервиса G-Core Meet. Пригодился и backoffice для старой системы: его удалось адаптировать к МVP всего за пару дней. В результате MVP-версия системы на 80% закрывала функционал старой платформы. Сейчас она, кстати, используется и для ещё одной задачи — в один момент мы клонировали MVP для хелпдеска агентства электронного здравоохранения Люксембурга, чтобы администраторы там могли созваниваться с пользователями.
Когда MVP был готов, мы приступили к разработке полноценной новой платформы. В качестве основы для API использовали API Platform и ReactJS в связке с Next.js для client-side. Без интересных задач не обошлось.
1. Реализуем уведомления
Одна из сложностей возникла с уведомлениями. Поскольку клиентами API могли быть как мобильный приложения, так и наши SPA, требовалось комбинированное решение.
Сначала мы выбрали Mercure Hub, с которым клиенты взаимодействуют через SSE (Server Sent Event). Но как бы это решение не пиарили сами создатели API Platform, наша мобильная команда его забраковала, так как с ним получать уведомления приложение могло только в активном состоянии.
Так мы пришли к Firebase, с помощью которого удалось добиться поддержки нативных пушей на мобильных устройствах, а для браузерных приложений оставили Mercure Hub. Теперь, когда в системе происходило какое-то событие, мы уведомляли пользователя об этом через нужный нам private-канал в Mercure Hub и, помимо этого, отправляли пуш ещё и на Firebase для мобильного устройства.
Почему мы сразу не реализовали всё на Firebase? Тут всё просто: несмотря на поддержку web-клиентов, браузеры без Push API — тот же Safari и большинство мобильных браузеров — с ним не работают. Однако, мы ещё планируем реализовать пуш-уведомления от Firebase для тех пользователей, которые используют поддерживаемые браузеры.
2. Функциональные тесты
Другая интересная ситуация возникла, когда мы занимались функциональными тестами для API. Как известно, каждый из них должен работать в чистом окружении. Но каждый раз поднимать базу и заполнять в ней базовые fixtures + fixtures, необходимые для тестирования, оказалось накладно с точки зрения производительности. Чтобы этого избежать, мы решили на начальном этапе поднимать базу данных на основании маппинга сущностей (
bin/console doctrine:schema:create
) и уже затем добавлять базовые fixtures (bin/console doctrine:fixtures:load
).Затем с помощью расширения dama/doctrine-test-bundle мы добились того, чтобы выполнение каждого теста оборачивалось в транзакцию и в конце тест-кейса откатывало её без фиксации. Благодаря этому, даже если в ходе тестирования в базу вносятся изменения, они не фиксируются, а база после прогона остаётся в том же состоянии, что и до запуска PHPUnit.
Чтобы на каждый эндпоинт был написан хотя бы один тест, мы сделали auto review тест. Он обнаруживает все зарегистрированные роуты и проверяет наличие тестов для них. Так, например, для роута app_appointment_create он проверяет, есть ли в папке
tests/Functional/App/Appointment файл CreateTest.php
.Дополнительно за качеством кода следят PHP-CS-Fixer, php-cpd и PHPStan c расширениями типа phpstan-strict-rules.
3. Клиентская часть
Для врачей и пациентов мы создали два независимых клиентских приложения с одинаковым UI и схожим возможностями. Чтобы наладить переиспользование функционала и UI в них, мы решили применить монорепозиторий, который включает в себя библиотеки и приложения. При этом сами приложения собираются и деплоятся независимо друг от друга, но могут зависеть от одних и тех же библиотек.
Такой подход позволяет в одном пул-реквесте создать фичу (библиотеку) и интегрировать её во все нужные приложения. Это преимущество и привело к использованию монорепозитория вместо реализации фич в отдельных npm-библиотеках и проектов в разных репозиториях.
Для конфигурации монорепозитория используется библиотека ns.js, которая с коробки позволяет собирать библиотеки и приложения React и Next.js, а именно этот стек и используется в проекте. Чтобы следить за качеством кода, мы используем ESLint и Prettier, для написания unit-тестов — Jest, а для тестирования компонентов React — React Testing Library.
Доктор прибыл: что получилось в итоге
Всего за пять месяцев все проблемы были разрешены, а новая платформа стала доступна пользователям любых устройств: мы подготовили веб-версию сервиса, а также мобильные приложения для iOS и Android.
Вот уже как 4 месяца сервис позволяет пациентам получать онлайн-консультации докторов и стоматологов. Они могут проходить как в аудио, так и видеоформате. В результате врачи выписывают рецепты и безопасно обмениваются медицинскими документами и результатами анализов с пациентами.
Платформа доступна для всех медицинских профессионалов, жителей и работников Люксембурга. Сейчас она работает в 2-х крупнейших учреждениях здравоохранения страны — в Больнице им. Роберта Шумана (Hopitaux Robert Schuman) и Больничном центре им. Эмиля Мэйриша (Centre Hospitalier Emile Mayrisch). Сервис развёрнут в защищённой точке присутствия облака G-Core Labs в люксембургском дата-центре EDH Tier IV, где для него настроена виртуальная среда в соответствии с нужными спецификациями.
qbtarzan
самое интересное не рассказали — на чем реализован мессаджинг и видеозвонки (XMPP/SIP, WebRTC ?), как решали вопросы с HIPAA compliance или его аналогом для EU, с какими проблемами и решениями столкнулись при горизонтальном масштабировании?
VG_GC Автор
Спасибо за вопрос! Звонки реализованы на WebRTC, чаты — Node.js + WebSocket.
В рамках требований по соответствию нормативным актам персональные данные, включая сообщения в чатах, медицинские заключения, хранятся в зашифрованном виде, логируется доступ и изменение sensitive data, БД в Люксембурге.
По масштабированию проблем не возникает, так как все сервисы докерезированы, и большинство из них stateless, можно реплицировать столько, сколько нужно.