Уроки электронного голосования в Московскую Городскую Думу 2019 года

Продолжаем говорить о деятельности ДИТ Москвы (смотрите наши предыдущие посты), но одновременно и переходим к следующей теме, внезапно ставшей актуальной и набравшей обороты — теме электронного голосования.

Если в прошлом году дистанционное голосование граждан рассматривалось скорее как курьёз или эксперимент, выводы из которого будут сделаны когда-то потом, то в 2020-м мы все внезапно обнаружили, что это — реальность, с которой нам предстоит столкнуться скоро и в полном объёме. В значительной степени к этому подтолкнули карантинные ограничения — проведение выборов оказалось под вопросом, а жизнь политических партий встала на паузу.

В общем, спрос встретился с предложением — и повалили законопроекты. Партиям разрешили проводить праймериз через Госуслуги/ЕСИА, регионы один за другим рапортуют о стремлении организовать ДЭГ (дистанционное электронное голосование) уже в сентябре на местных выборах.

К счастью или к сожалению, одного только законотворчества мало — нужна ещё и техническая реализация, с которой не всё так однозначно. Кто-то верит, что всё уже было сделано в Москве в прошлом году (там даже был блокчейн!), кто-то — что ДЭГ вообще невозможно реализовать на уровне, не допускающем массовых фальсификаций, а потому от ДЭГ необходимо отказаться в принципе.

Поэтому мы решили посвятить разбору этих вопросов небольшой цикл статей и встреч:

  1. Алексей Щербаков — «Уроки электронного голосования в Московскую Городскую Думу 2019 года»
  2. Олег Артамонов — «Дистанционные электронные голосования: построение доверенной системы»
  3. Круглый стол «Нажми на кнопку: теория и практика электронных голосований»

Первую статью начинаем прямо сейчас, а вторую и анонс круглого стола повесим завтра (и добавим сюда ссылки).

Строго говоря, это не статья, а слегка литературно отредактированный текст одноимённого выступления Алексея Щербакова (alexeishch) на нашей конференции 5 марта этого года.

Алексей — приглашенный эксперт команды Романа Юнемана по подготовке доклада «Электронное голосование. Риски и уязвимости», ведущий backend-разработчик компании FoodPlex.

В докладе рассказывается о том, как именно было технически устроено дистанционное электронное голосование на выборах в МГД 2019 года, какие были достоинства и недостатки как в технических решениях, так и в работе с экспертными группами.

Помимо этого текста, можно прочитать также ещё две статьи, уже публиковавшиеся на Хабре:

Если вы предпочитаете видео или аудио, полную запись доклада можно посмотреть на YouTube.

***

Здравствуйте, меня зовут Алексей Щербаков, я был экспертом приглашённой команды Романа Юнемана на Московском голосовании. Ну вообще прежде чем рассказывать о самом голосовании, стоит сказать как это вообще происходило.

Всё это началось в марте 2019 года, когда было объявлено об эксперименте, потом уже в мае принят закон о голосовании, а само голосование происходило 8 сентября в трёх округах Москвы. Голосование проходило через интернет в течение 12 часов.

Сама система была построена на базе блокчейна Ethereum фирмы Parity. Там же была использована схема Эль-Гамаля для шифрования. Система логирования использовалась Graylog и для передачи данных между сообщениями использовался какая-то реализация AMQP, мы предполагаем, что скорее всего это был RabbitMQ, просто как корпоративный стандарт. Сама система выглядела вот таким образом:

Большая часть системы находилась вне Департамента информационных технологий (ДИТ) Москвы [это очень важный момент, так как с внешними экспертами общался только ДИТ Москвы — прим. ред.], но блокчейн находился у них. Они работали совместно с порталом Госуслуг. Описание, как эта система создавалась, ДИТ Москвы публиковал на Хабре. И они там же уже говорили в частности о том, что у них проблемы были, в основном, только один час, порядка 400 человек были этим затронуты.

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

Прежде чем сказать по поводу анализа, который был сделан, расскажу, как мы подходили к задаче. Если бы я сам проектировал эту систему, то я бы использовал определенные стандарты для проектирования системы высокой доступности. В частности, использовались бы метрики для наблюдения непосредственно за здоровьем системы. Для того, чтобы понимать, что начинаются какие-то проблемы — и на них быстро реагировать. И помимо команды, которая должна всё это делать, это должны видеть сами наблюдатели — то есть, наблюдатели должны каким-то образом понимать, что что-то идёт не так. И участковая-избирательная комиссия, которая находилась на этом участке, тоже должна была понимать, что происходило не так, если вдруг что-то идёт не так.

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

Если мы рассмотрим вторую метрику — число транзакций на блок — то по ним мы видим проблему уже подробнее. Мы, во-первых, видим, что в зонах отключения, никаких транзакций не записывалось. В нашей подозрительной зоне мы видим крайне мало транзакций, а потом мы видим интересный момент, когда у нас меняется характер записи данных блокчейна. С чем это связано? Изначально, когда данные писались, они писались с определённым интервалом, это сделано для того, чтобы нельзя было по времени голосования точно определить, какой человек голосовал. Данные накапливались и сбрасывались в блокчейн. Однако потом после какой-то реконфигурации блокчейна данные стали записываться уже хаотично. То есть была произведена какая-то операция, но мы не можем точно сказать на основе этой метрики что конкретно ДИТ сделал. Но мы можем сказать, что в данном случае ДИТ каким-то образом вмешался в работу системы.

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

Помимо этого, у нас ещё есть ещё одна зона, в которой данные не доходили до блокчейна. Суммарно все эти зоны неисправности занимают 4 часа. Зона деградации занимает порядка 6 часов, она началась после обеда и шла до конца голосования. Из-за того, что никак не мониторили блокчейн, они даже не подозревали о том, что были какие-то проблемы. Более того, люди, которые присутствовали на самом участке, часть избирательной комиссии, говорили, что всё, что они могли сделать — это сидеть на диванчике и смотреть, что происходит на экране. То есть они не понимали, что происходит, и узнавали о каких-то проблемах исключительно из СМИ. У них не было вообще никаких инструментов для того, чтобы наблюдать за проблемой.

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

Вот так выглядели экраны, там просто четыре позиции: классическая «воронка продаж», когда у нас есть количество людей, которые перешли на страницу голосования, авторизовались, получили бюллетень и проголосовали, и оно с каждым шагом уменьшается.

Здесь есть очень важный момент — время жизни бюллетеня. Если избиратель не успевал за 15 минут заполнить бюллетень, то он считался аннулированным. И сама статистика также шла интервалами по 15 минут. То есть, если у нас избиратель не проходил какой-то участок воронки за 15 минут, то мы можем уверенно сказать, что на следующем этапе статистики он не учитывался. И на каждом этапе у нас получалось меньшее количество. Именно благодаря этому удалось отследить интересные аномалии статистики.

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

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

Помимо статистики, у нас есть интересная аудиозапись — время около 17 часов, проголосовало около 2000 человек, один из представителей ДИТ Москвы рассказывает, какие вмешательства они проводили на работающей системе. В частности он рассказывает, что порядка 900 человек повторно получали SMS для авторизации.

Нам это говорит, во-первых, о том, что благодаря системе логирования, которую они использовали, ДИТ Москвы мог нарушать тайну голосования. Они могли сопоставить время голосования, статус бюллетеня и номер телефона, что очень важно! Они идентифицировали людей, у которых были проблемы, определили их номера телефонов и разослали повторные SMS. Количество этих людей — порядка 40 % от всех проголосовавших на этом участке. Разница между двумя кандидатами, первым и вторым, составила всего 84 человека, в то время как для 900 человек мы даже не можем сказать, какой у них был результат. Потому что над ними производились какие-то действия. Мы не можем сказать, что эти голоса подтасовывали, но мы можем сказать, что у 900 человек были проблемы, мы не можем сказать, за кого они проголосовали и проголосовали ли они вообще. То есть число людей, которые столкнулись с проблемами, в десять раз больше, чем число людей, которые отделяли одного кандидата от победы.

Репозиторий с данными и код, который используется для анализа, можно найти по этой ссылке.

Также мы проивели анализ кода, который использовался для самого голосования. Мы ожидали, что большинство операций будет происходить непосредственно в самом блокчейне и что код должен быть опубликован. Мы получили смарт-контракты, код формы и код, ответственный за отправку сообщения. Но там были части, которые остались неизвестными, потому что они выполнялись на стороне уже другого Департамента — уже портала mos.ru.

Что было интересно найдено в коде? Оказалось, что он не ограничивал возможность одного человека проголосовать в разных округах. Это интересный момент, это было на откуп отдано бэкенду, который находился где-то в другом месте и исходный код которого мы не смогли посмотреть. Непонятно, зачем в системе использовали блокчейн — так как он всё равно не контролировал всё, его можно было заменить на обычную базу данных. Ну и в код формы буквально за день до голосования добавили вот такой волшебный код, который позволял с помощью одной переменной на стороне бэкенда включать дополнительный скрипт в форму, что очень интересно! Зачем они это сделали? По сути, это возможность выполнения произвольного кода в момент голосования на устройстве пользователя.

По криптографии также был интересный момент. Изначально они выбрали 256-бит шифрование, хотя ещё в 1999 году для этой схемы предлагалось использовать 768 бит, а 10 лет назад предлагалось уже 1024 бита. А если сейчас открыть рекомендации Евросоюза, то там будет требование «не менее 1024 бит», если же требуется защита до 2030-го года, то рекомендуется использовать 3072 бита. Есть также интересный момент в том, как они вычисляют энтропию. Понятно, что люди не до конца разобрались, зачем им это всё нужно.

Что я могу сказать об этой системе?

Во-первых, ДИТ Москвы не смог обеспечить хотя бы 90% работоспособности. Вообще считается, что система высокой доступности, она должна иметь не менее 90% времени работы. То есть мы не можем даже сказать, что она была рабочая.

Во-вторых, на продакшн-системе производились операции, которые никто никак не контролировал, никто вообще не мог понять, что происходило. Если посмотреть заседание суда [по обжалованию итогов выборов — прим. ред.], окажется, что ни люди, ни наблюдатели, ни сама комиссия не понимали, что происходит. Всё-таки надо было их как-то подготовить к самой процедуре, которая происходила.

Вместо заключения

Мы не хотим сказать, что электронные выборы — это обязательно бардак, постоянные проблемы, странные технические решения и непонимание, что вообще происходит в данный момент.

Тем не менее, как мы видим по этому материалу, вопрос доверия к результатам голосования по сути своей являлся вопросом доверия к его организаторам — взаимодействие с которыми оказалось весьма неоднозначным. Это, как нам кажется, отвечает и на весьма актуальный вопрос о том, имеет ли ДИТ Москвы достаточно опыта для его масштабирования на всю Москву, а тем более — всю страну, и на вопрос о том, можно ли просто взять какую-нибудь «голосовалку с блокчейном», запустить её на сервере и начать проводить выборы.

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

ссылка на оригинал статьи https://habr.com/ru/company/analogbytes/blog/504328/

Как мы отказались от использования Styled-System для создания компонентов и изобрели собственный велосипед

Всем привет! Меня зовут Саша, я сооснователь и по совместительству главный разработчик в Quarkly. В этой заметке я хочу рассказать о том, как концепция атомарного CSS, которой мы придерживаемся, вкупе с недостатками функционала Styled-System (и Rebass, как частного случая использования этой библиотеки) сподвигли нас к созданию своего собственного инструмента, который мы назвали Atomize.

Небольшая преамбула. Наш проект Quarkly — это микс графического редактора (вроде Figma, Sketch) и конструктора сайтов (по типу Webflow) с добавлением функционала, присущего классическим IDE. Про Quarkly мы обязательно напишем отдельный пост, там есть про что рассказать и что показать, ну а сегодня речь пойдет про упомянутый выше Atomize.

Atomize лежит в основе всего проекта и позволяет нам решать задачи, которые было бы невозможно или трудно решить с помощью Styled-System и Rebass. Как минимум, решение было бы гораздо менее изящным.

Если мало времени, чтобы осилить весь пост сейчас, то более лаконично ознакомиться с Atomize можно у нас на GitHub.

А чтобы знакомство было приятнее, мы запускаем конкурс по сборке react-компонентов с использованием Atomize. Подробнее об этом в конце поста.

С чего всё началось

Начиная разрабатывать Quarkly, мы условились, что хотим дать нашему пользователю возможность верстать на компонентах, но без необходимости использовать отдельный CSS-файл. Чтобы код был максимально минималистичен, но сохранял все возможности CSS, в отличие от инлайновых стилей.

Задача не инновационная и, на первый взгляд, вполне решаемая с помощью Styled-System и Rebass. Но этой функциональности нам оказалось недостаточно, а кроме того мы столкнулись со следующими проблемами:

  • неудобная работа с брейкпоинтами;
  • отсутствие возможности писать стили на состояние hover, focus etc;
  • механизм работы с темами показался нам недостаточно гибким.

Что представляет собой Atomize (кратко)

image

Из ключевых особенностей Atomize мы можем выделить следующие:

  • возможность использования переменных из темы в составных css-свойствах;
  • поддержка hover и любых других псевдоклассов;
  • короткие алиасы на каждое свойство (как в emmet);
  • возможность указывать стили на конкретный брейкпоинт, сохраняя при этом читаемость разметки;
  • минималистичный интерфейс.

При этом у Atomize есть два основных предназначения:

  • создание компонентов с поддержкой атомарного CSS и тем;
  • создание виджетов для интерактивного редактирования в проекте Quarkly.

Atomize, инструкция по применению

Перед началом работы необходимо установить зависимости:

npm i react react-dom styled-components @quarkly/atomize @quarkly/theme

Atomize является оберткой вокруг styled-component и имеет похожий API. Достаточно вызвать метод с именем необходимого элемента:

import atomize from '@quarkly/atomize';   const MyBox = atomize.div();

На выходе мы получаем react компонент, способный принимать любые CSS в виде пропсов.
Для удобства использования была разработана система алиасов свойств. К примеру bgc === backgroundColor

ReactDOM.render(<MyBox bgc="red" />, root);

С полным списком свойств и алиасов можно ознакомиться здесь.

Также предусмотрен механизм наследования в React:

const MySuperComponent = ({ className }) => {    // some logic here    return <div className={className} />; };   const MyWrappedComponent = atomize(MySuperComponent);

Работа с темами

Про это, как мне представляется, следует рассказать подробнее. Темы в Quarkly базируются на CSS-переменных. Ключевой особенностью является возможность переиспользования переменных из тем как в пропсах, так и в самой теме, без необходимости использования дополнительных абстракций в виде template-функций и последующей дополнительной обработки со стороны пользователя.

Чтобы использовать переменные из темы, достаточно описать свойство в теме и обратиться к этому свойству, используя префикс "—".

Переменные можно использовать как в JSX:

import Theme from "@quarkly/theme";   const theme = {    colors: {        dark: "#04080C",    }, }; export const MyComp = () => (    <Theme>        <Box bgc="--colors-dark" height="100px" width="100px" />    </Theme> );

(Цвет #04080C доступен через свойство —colors-dark)

Так и в самой теме:

import Theme from "@quarkly/theme";   const theme = {    colors: {        dark: "#04080C",    },    borders: {        dark: "5px solid --colors-dark",    }, }; export const MyComp = () => (    <Theme>        <Box border="--borders-dark" height="100px" width="100px" />    </Theme> );

(Мы переиспользовали переменную из цветов, подключив её в тему borders)

Для цветов в JSX-разметке предусмотрен упрощенный синтаксис:

import Theme from "@quarkly/theme";   const theme = {    colors: {        dark: "#04080C",    }, }; export const MyComp = () => (    <Theme>        <Box bgc="--dark" height="100px" width="100px" />    </Theme> );

Для работы с медиа-выражениями в темах предусмотрен breakpoint.
К любому свойству можно добавить префикс в виде имени ключа breakpoint’а.

import Theme from "@quarkly/theme";   const theme = {    breakpoints: {        sm: [{ type: "max-width", value: 576 }],        md: [{ type: "max-width", value: 768 }],        lg: [{ type: "max-width", value: 992 }],    },    colors: {        dark: "#04080C",    },    borders: {        dark: "5px solid --colors-dark",    }, }; export const MyComp = () => (    <Theme>        <Box            md-bgc="--dark"            border="--borders-dark"            height="100px"            width="100px"        />    </Theme> ); 

С исходным кодом тем можно ознакомиться здесь.

Эффекты

Основным отличием Atomize от Styled-System являются «effects». Что это и зачем это нужно?
Давайте представим, что вы создаете компонент Button, меняете у него color и border, но как назначить стили на hover, focus etc? Тут на помощь приходят эффекты.

При создании компонента достаточно передать объект с конфигурацией:

const MySuperButton = atomize.button({  effects: {    hover: ":hover",    focus: ":focus",    active: ":active",    disabled: ":disabled",  }, });

Ключом является префикс в имени пропса, а значением — CSS-селектор. Таким образом мы закрыли потребность во всех псевдо-классах.

Теперь если мы укажем префикс hover к любому CSS-свойству, то оно будет применено при определенном эффекте. Например, при наведении курсора:

ReactDOM.render(<MySuperButton hover-bgc="blue" />, root);

Также эффекты можно сочетать с медиа-выражениями:

ReactDOM.render(<MySuperButton md-hover-bgc="blue" />, root);

Несколько примеров

Чтобы визуализировать информацию выше, давайте теперь соберем какой-нибудь интересный компонент. Мы приготовили два примера:

Во втором примере мы задействовали большую часть функционала, а также внешний API.

Но это не всё

Второе предназначение Atomize, как вы упомянули выше, это создание виджетов в Quarkly на основе пользовательских react-компонентов.

Для этого достаточно обернуть ваш компонент в Atomize и описать его конфигурацию, чтобы Quarkly смог понять, какие свойства можно интерактивно редактировать:

export default atomize(PokemonCard)(  {    name: "PokeCard",    effects: {      hover: ":hover",    },    description: {      // past here description for your component      en: "PokeCard — my awesome component",    },    propInfo: {      // past here props description for your component      name: {        control: "input",      },    },  },  { name: "pikachu" } );

Поля конфигурации для компонента выглядят так:

  • effects — определяет браузерные псевдоклассы (hover, focus, etc);
  • description — описание компонента, которое будет появляться при наведении курсора на его название;
  • propInfo — конфигурация контролов, которые будут отображаться в правой панели (вкладка props).

Как определить пропсы, которые будут выводиться на правой панели (вкладка props):

propInfo: {    yourCustomProps: { // имя свойства        description: { en: "test" }, // описание с учетом локализации        control: "input" // тип контрола    } }

Возможные варианты контролов:

  • input,
  • select,
  • color,
  • font,
  • shadow,
  • transition,
  • transform,
  • filter,
  • background,
  • checkbox-icon,
  • radio-icon,
  • radio-group,
  • checkbox.

Ещё один пример. Здесь мы добавили свой компонент в систему и теперь можем редактировать его интерактивно:

Спасибо тем, кто осилил материал до конца! Заранее извиняюсь за сумбур, это первый опыт написания такого рода статей. Буду благодарен за критику.

А теперь конкурс!

Дабы слегка подогреть интерес сообщества к более тесному знакомству с Atomize, мы решили пойти по простому и понятному (как и сам Atomize) пути — мы запускаем конкурс!

Вся информация о сроках, правилах и призах доступна на официальном сайте конкурса.

Если коротко: для участия и победы необходимо придумать (или найти готовый) интересный и полезный компонент на React и адаптировать его под требования Atomize. Мы выберем и наградим победителей сразу в нескольких номинациях. Дополнительные призы от нашей команды в случае добавления вашего компонента в Quarkly гарантированы.

ссылка на оригинал статьи https://habr.com/ru/company/quarkly/blog/504064/

Полезного пост: Все самые актуальные курсы, трансляции и техтоки

Окей, мы инновационная ИТ-компания, а значит у нас есть разработчики – и это хорошие разработчики, увлеченные своим делом. А еще они проводят live streaming, и все вместе это называется DevNation.

Ниже просто полезные ссылки на живые мероприятия, видео, митапы и техтолки.

Поучиться

1 июня
Мастер-курс «Kubernetes для новичков» – доступен на английском, испанском, португальском и французском языках

3 июня
Мастер-курс «Основы Kubernetes» – доступен на английском, испанском, португальском и французском языках

Course: Get started with Red Hat Enterprise Linux (4 lessons, 35 minutes)
Основы дорогого нашему сердцу Red Hat Enterprise Linux, его использование с такими инструментами, как Podman, Buildah и SQL.

Курс Основы OpenShift – 11 уроков, 195 минут. Инструменты и методы, используемые при создании и развертывании приложений.

Поболтать

29 мая
Tech Talk @ 13:00 UTC: jbang: сила Java при написании shell-сценариев

4 июня
Tech Talk @ 16:00 UTC: Машинное обучение с использованием Apache Spark на платформе Kubernetes

Tech Talk @ 17:00 UTC: Машинное обучение с использование Jupyter Notebooks на базе Kubernetes и OpenShift

5 июня
Tech Talk @ 13:00 UTC: Обновления Apache Camel 3

Чудеса на виражах

Абсолютно бесплатный онлайн курс про OpenShift Applications – 30 дней видео и текстового контента, плюс 10 часов основанных на реальных событиях лабораторных.

Бесплатная электронная книга: The Knative Cookbook
Про то, как решать общие проблемы при создании, развертывании и управлении serverless приложений с Kubernetes и Knative.

Посмотреть в тишине

Видео: 4K-Kubernetes с Knative, Kafka и Kamel – 40 минут
В честь запуска Knative Cookbook – стриминг живого кода самых крутых техник на основе Knative, которые мы только можем себе представить, включая Кафку и Kamel.

Видео: Kubernetes made easy with OpenShift | DevNation Tech Talk (32 minutes)
Сначала разворачиваем приложение в Kubernetes, а потом различными способами разворачиваем его в openShift.

Video: Linux cheat codes | DevNation Tech Talk (34 minutes)
Советы, хитрости и how-to про Linux, которые в совокупности дополняют чит-коды, необходимые для начала освоения операционной системы Linux

Video: Scott McCarty introduces Red Hat Universal Base Images (3 minutes)
Scott McCarty представляет Red Hat Universal Base Images (UBI) создавая образ контейнера в Fedora, а затем разворачивая его в Red Hat Enterprise Linux (RHEL) 8. DIY видео!

Video: Building freely distributed containers with open tools | DevNation Tech Talk (32 minutes)
Как создавать и запускать контейнеры на основе Red Hat Universal Base Images, используя только учетную запись обычного пользователя — без демона (no daemon), без root (rootless), без суеты (голосом Меладзе) — и Podman.

На русском языке

Записи вебинаров

Red Hat OpenShift Container Storage
Red Hat OpenShift Container Storage – это решение хранения, которое было разработано специально для контейнерных инфраструктур, а также тесно интегрировано с Red Hat OpenShift Container Platform, что позволило получить единый интерфейс управления и доступа к данным.

Это Quarkus — Kubernetes native Java фреймворк
Quarkus – это «Java-фреймворк следующего поколения, ориентированный на Kubernetes» с открытым исходным кодом. Он обеспечивает очень быстрое время загрузки приложения и низкое потребление памяти. Это делает Quarkus идеально подходящим для рабочих нагрузок Java, выполняемых в качестве микросервисов в Kubernetes и OpenShift, а также для рабочих нагрузок Java, выполняемых в виде serverless-функций.

Вживую

4 июня – Решения HPE и Red Hat для SAP HANA
Переход на SAP HANA это задача непростая, требующая тщательной подготовки и планирования. Компания HPE имеет богатый совместный опыт в реализации таких проектов и готова предложить свои услуги по планированию миграции, выбору правильной конфигурации и внедрению решения, соответствующего индивидуальным потребностям наших заказчиков. Сочетание интеллектуальной операционной среды Red Hat с дополнительными инструментами управления контентом SAP HANA, Red Hat Enterprise Linux for SAP Solutions, обеспечат единую согласованную основу для рабочих нагрузок SAP.

9 июня – Вебинар про автоматизацию сетей
Ansible использует модель данных (сценарий или роль), которая отделена от уровня исполнения. С помощью Ansible вы сможете с легкостью автоматизировать разнородное сетевое оборудование, пользуясь наработками сообщества и высококвалифицированной поддержкой от Red Hat.

ссылка на оригинал статьи https://habr.com/ru/company/redhatrussia/blog/504332/

Насколько хорошо вы знаете JavaScript?

Доброго времени суток! Как известно, одной из характерных черт JavaScript, наряду c мультипарадигменностью, слабой (динамической) типизацией, автоматическим управлением памятью и прототипным наследованием, является тот факт, что JS — это однопоточный (синхронный) язык.

Что касается синхронности, то ключевым элементом здесь выступает стек вызовов (call stack). Если вы впервые о нем слышите, то настоятельно рекомендую прочитать эту статью и посмотреть это видео.

Насколько хорошо вы знакомы с тем, как работает JS под «катом»?

Давайте проверим.

Предлагаю вашему вниманию небольшой интерактив — игру под названием «CallStack Challenge».

Условия следующие: 11 вопросов на определение порядка вывода значений в консоль, 11 ответов в формате «log,log,log», +1 балл за каждый ответ. Набрали 9+ баллов, значит, вы — мастер коллстека. Набрали меньше — есть над чем работать.

Готовы? Тогда вперед.

Проект на GitHub Pages.

Код проекта на GitHub

Благодарю за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/504334/

Сказ о том, как я настраивал Azure AD B2C на React и React Native Часть 2 (Туториал)

image

Предисловие

Продолжение цикла по работе с Azure B2C. В данной статье я расскажу о самом сложном и неочевидном моменте, а именно Identity Experience Framework.

Основная цель — собрать воедино картинку для тех кто вообще не в теме и помочь настроить какие-то основные фичи.

Ссылки на связанные посты

Базовая настройка

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

  1. Заходим в Identity Experience Framework
  2. Нажимаем отправить пользовательскую политику
  3. Выбираем файл (не забываем нажать «Перезаписать настраиваемую политику, если она уже существует»)
  4. Отправляем

image

По сути, с прошлого раза ничего не изменилось, НО:

Если вы меняете файл TrustFrameworkExtension.xml или TrustFrameworkBase.xml — переодически загружайте файл, который на них ссылается.
Иногда, когда вы производите изменения в одном из этих файлов, тестируете, происходит так, что ваши изменения не появляются. Это происходит из за того, что в базовом файле
вы поменяли что-то так, что при проверке приведет к ошибке дочерний файл.

В прошлой статье мы остановились на том — что добавили следующие файлы:

a.TrustFrameworkBase.xml
b.TrustFrameworkExtensions.xml
c.SignUpOrSignin. XML
d.ProfileEdit. XML
e.PasswordReset. XML

Теперь мне бы хотелось рассказать подробно о каждом из них.

TrustFrameworkBase.xml

Данный файл содержит в себе базовую настройку. По сути он — основа основ, но в туториалах про него в основном говорят «Лучше не трогайте этот файл». Отчасти это правда, но есть несколько моментов о которых не говорят:

  1. Любой туториал, который говорит произвести изменения в TrustFrameworkExtensions.xml по сути своей перезаписывает правила из TrustFrameworkBase.xml
  2. Есть ситуации, когда удобнее поменять что-то в TrustFrameworkBase.xml.
  3. Если вы найдете в других файлах ссылку на объект, который отсутствует в этих файлах — то он 100% лежит в TrustFrameworkBase.xml и его можно открыть и посмотреть

Из моего опыта скажу — я поменял в этом файле всего две вещи (Локализацию и удалил одно поле).

TrustFrameworkExtension.xml

С данным файлом вы будете проводить много времени вместе. По сути, это основной файл для ваших настроек. О нем постоянно упоминается в туториалах.

SignUpOrSignin. XML, ProfileEdit. XML, PasswordReset. XML

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

Теперь поговорим о структуре файлов. У всех файлов похожая структура, поэтому описывать я буду на основе файла TrustFrameworkExtension.xml.

Файл поделен на несколько основных блоков

<TrustFrameworkPolicy>   <BasePolicy> <!-- Ссылка на основной файл -->       <TenantId>customtenant.onmicrosoft.com</TenantId>       <PolicyId>B2C_1A_TrustFrameworkBase</PolicyId>   </BasePolicy>    <BuildingBlocks> <!-- Элементы из которых мы будем строить UI -->   </BuildingBlocks>    <ClaimsProviders> <!-- Собрание страниц воедино и добавление данных в JWT token) -->   </ClaimsProviders>      <UserJourneys> <!-- Переходы и передача данных между страницами -->   </UserJourneys> </TrustFrameworkPolicy> 

Теперь о каждом блоке отдельно.

BuildingBlocks

В этом блоке мы добавляем «инструменты», которые мы сможем использовать в дальнейшей работе.

ClaimsSchema

Элемент ClaimsSchema определяет типы утверждений, на которые можно ссылаться в рамках политики.

  <BuildingBlocks>     <ClaimsSchema>       <ClaimType Id="picture"><!-- Добавляем картинку как возможный элемент UI или данные для токена -->         <DisplayName>Picture</DisplayName>         <DataType>string</DataType>       </ClaimType>       <ClaimType Id="country"><!-- Пример добавления выпадающего списка -->         <DisplayName>Country</DisplayName>         <DataType>string</DataType>         <UserInputType>DropdownSingleSelect</UserInputType>         <Restriction>           <Enumeration Text="Russia" Value="russia" SelectByDefault="false" />           <Enumeration Text="Other" Value="other" SelectByDefault="false" />         </Restriction>       </ClaimType>            ...     </ClaimsSchema>   

Predicates

Предикаты и элементы предикат валидатионс позволяют выполнять проверку, чтобы убедиться, что в клиент Azure Active Directory B2C (Azure AD B2C) введены только правильно сформированные данные.

    <Predicates> <!-- С помощью предикатов устанавливаются правила, в данном случае правила проверки пароля -->       <Predicate Id="LengthRange" Method="IsLengthRange">         <UserHelpText>The password must be between 6 and 64 characters.</UserHelpText>         <Parameters>           <Parameter Id="Minimum">6</Parameter>           <Parameter Id="Maximum">64</Parameter>         </Parameters>       </Predicate>       <Predicate Id="Lowercase" Method="IncludesCharacters">         <UserHelpText>a lowercase letter</UserHelpText>         <Parameters>           <Parameter Id="CharacterSet">a-z</Parameter>         </Parameters>       </Predicate>           ...     </Predicates>   

PredicateValidations

В то время как предикаты определяют проверку на соответствие типу утверждения, PredicateValidations группирует набор предикатов для формирования проверки ввода пользователя, соответствующей типу утверждения.

    <PredicateValidations> <!-- Говорим по каким правилам проверять инпут -->       <PredicateValidation Id="CustomPassword">         <PredicateGroups>           <PredicateGroup Id="LengthGroup">             <PredicateReferences MatchAtLeast="1">               <PredicateReference Id="LengthRange" />             </PredicateReferences>           </PredicateGroup>           <PredicateGroup Id="CharacterClasses">             <UserHelpText>The password must have at least 1 of the following:</UserHelpText>             <PredicateReferences MatchAtLeast="2">               <PredicateReference Id="Lowercase" /> <!-- Ссылаемся на правила, которые добавили выше -->               <PredicateReference Id="Uppercase" />                     ...             </PredicateReferences>           </PredicateGroup>         </PredicateGroups>       </PredicateValidation>     </PredicateValidations>   

ClaimsTransformations

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

    <ClaimsTransformations> <!-- Превращаем одни данные в другие -->       <ClaimsTransformation Id="GenerateSendGridRequestBody" TransformationMethod="GenerateJson">         <InputClaims>           <InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.to.0.email" />           <InputClaim ClaimTypeReferenceId="otp" TransformationClaimType="personalizations.0.dynamic_template_data.otp" />           <InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.dynamic_template_data.email" />         </InputClaims>         <InputParameters>           <InputParameter Id="template_id" DataType="string" Value="d-b0000000000000000000000000000000" /> <!-- Template ID SendGrid (это относится к костюмному письму) -->           <InputParameter Id="from.email" DataType="string" Value="custom@email.com" />           <InputParameter Id="personalizations.0.dynamic_template_data.subject" DataType="string" Value="Welcome to Habr!"/>         </InputParameters>         <OutputClaims>           <OutputClaim ClaimTypeReferenceId="sendGridReqBody" TransformationClaimType="outputClaim" />         </OutputClaims>       </ClaimsTransformation>             ...     </ClaimsTransformations>   

ContentDefinitions

Позволяет задать шаблоны для каждой из ваших страниц

    <ContentDefinitions> <!-- Тут указываем какой шаблон использовать для вашей страницы -->       <ContentDefinition Id="api.signuporsignin">         <LoadUri>https://azure.blob.core.windows.net/yourblobstorage/pagelayoutfile.html</LoadUri>         <RecoveryUri>~/common/default_page_error.html</RecoveryUri>         <DataUri>urn:com:microsoft:aad:b2c:elements:contract:unifiedssp:1.2.0</DataUri>       </ContentDefinition>           ...     </ContentDefinitions>   

DisplayControls

Элемент управления «Отображение» — это элемент пользовательского интерфейса, который имеет специальные функции и взаимодействует с серверной службой Azure Active Directory B2C (Azure AD B2C)

    <DisplayControls> <!-- Создаем кнопку для превращения данных -->       <DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">         <DisplayClaims> <!-- Отображаемое поле -->           <DisplayClaim ClaimTypeReferenceId="email" Required="true" />           <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />         </DisplayClaims>         <OutputClaims> <!-- Выходные данные (которые можно будет добавить в токен) -->           <OutputClaim ClaimTypeReferenceId="email" />         </OutputClaims>         <Actions>           <Action Id="SendCode"> <!-- Действия которые выполняются при нажатии на кнопку -->             <ValidationClaimsExchange>               <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" />               <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendGrid" />             </ValidationClaimsExchange>           </Action>             ...         </Actions>       </DisplayControl>           ...     </DisplayControls>   </BuildingBlocks> 

ClaimsProviders

В этом блоке, мы будем создавать сами страницы, а точнее их наполнение. Тут мы будем указывать какие у страницы входные и выходные данные.

ClaimsProvider организует связь технических профилей с поставщиком утверждений.

  <ClaimsProviders>     <ClaimsProvider> <!-- Для примера - этот провайдер есть в бэйз и мы его полностью перезаписываем-->       <DisplayName>Self Asserted</DisplayName>     

Элемент TechnicalProfiles содержит набор технических профилей, поддерживаемых поставщиком утверждений.

      <TechnicalProfiles>         <TechnicalProfile Id="SelfAsserted-Social">           <DisplayName>User ID signup</DisplayName>           <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />           <Metadata>             <Item Key="ContentDefinitionReferenceId">api.selfasserted</Item> <!-- Тип страницы. (Из моего опыта если хотите сделать полностью новую страницу, лучше использовать его)-->           </Metadata>           <CryptographicKeys>             <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" /> <!-- Ключ из вашего контейнера с ключами -->           </CryptographicKeys>           <InputClaims> <!-- Входящие данные т.е. при переходе на эту страницу проверяем что поступили следующие данные-->             <InputClaim ClaimTypeReferenceId="givenName" />             <InputClaim ClaimTypeReferenceId="surname" />           </InputClaims>           <OutputClaims> <!-- Выходные данные. Некоторые получаются в результате работы технического профайла, другие в результате инпута юзера (ниже оставил оригинальные комментарии) -->               <!-- These claims ensure that any values retrieved in the previous steps (e.g. from an external IDP) are prefilled.                   Note that some of these claims may not have any value, for example, if the external IDP did not provide any of                  these values, or if the claim did not appear in the OutputClaims section of the IDP.                  In addition, if a claim is not in the InputClaims section, but it is in the OutputClaims section, then its                  value will not be prefilled, but the user will still be prompted for it (with an empty value). -->             <OutputClaim ClaimTypeReferenceId="objectId" />             <OutputClaim ClaimTypeReferenceId="newUser" />             <OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />              <!-- Optional claims. These claims are collected from the user and can be modified. If a claim is to be persisted in the directory after having been                   collected from the user, it needs to be added as a PersistedClaim in the ValidationTechnicalProfile referenced below, i.e.                   in AAD-UserWriteUsingAlternativeSecurityId. -->             <OutputClaim ClaimTypeReferenceId="givenName" Required="true"/>             <OutputClaim ClaimTypeReferenceId="surname" Required="true"/>             <OutputClaim ClaimTypeReferenceId="country" Required="true"/>           </OutputClaims>         </TechnicalProfile>       </ClaimsProvider>       

Пример добавления поставщиков удостоверений Facebook

Пример частичной перезаписи правила

    <ClaimsProvider> <!-- Маленький пример частичного наследования. Тут мы перезаписываем только некоторые параметры фэйсбука-->       <DisplayName>Facebook</DisplayName>       <TechnicalProfiles>         <TechnicalProfile Id="Facebook-OAUTH">           <Metadata>             <Item Key="client_id">FACEBOOK_ID</Item>             <Item Key="scope">email public_profile</Item>             <Item Key="ClaimsEndpoint">https://graph.facebook.com/me?fields=id,first_name,last_name,name,email,picture</Item>           </Metadata>           <OutputClaims>             <OutputClaim ClaimTypeReferenceId="picture" PartnerClaimType="picture" />           </OutputClaims>         </TechnicalProfile>       </TechnicalProfiles>     </ClaimsProvider>   

Полное правило из TrustFrameworkBase.xml

      <ClaimsProvider>         <!-- The following Domain element allows this profile to be used if the request comes with domain_hint               query string parameter, e.g. domain_hint=facebook.com  -->         <Domain>facebook.com</Domain>         <DisplayName>Facebook</DisplayName>         <TechnicalProfiles>           <TechnicalProfile Id="Facebook-OAUTH">             <!-- The text in the following DisplayName element is shown to the user on the claims provider                   selection screen. -->             <DisplayName>Facebook</DisplayName>             <Protocol Name="OAuth2" />             <Metadata>               <Item Key="ProviderName">facebook</Item>               <Item Key="authorization_endpoint">https://www.facebook.com/dialog/oauth</Item>               <Item Key="AccessTokenEndpoint">https://graph.facebook.com/oauth/access_token</Item>               <Item Key="HttpBinding">GET</Item>               <Item Key="UsePolicyInRedirectUri">0</Item>                  <!-- The Facebook required HTTP GET method, but the access token response is in JSON format from 3/27/2017 -->               <Item Key="AccessTokenResponseFormat">json</Item>             </Metadata>             <CryptographicKeys>               <Key Id="client_secret" StorageReferenceId="B2C_1A_FacebookSecret" />             </CryptographicKeys>             <InputClaims />             <OutputClaims>               <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="id" />               <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="first_name" />               <OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="last_name" />               <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />               <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />               <OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="facebook.com" AlwaysUseDefaultValue="true" />               <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />             </OutputClaims>             <OutputClaimsTransformations>               <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />               <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" />               <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />             </OutputClaimsTransformations>             <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin" />           </TechnicalProfile>         </TechnicalProfiles>       </ClaimsProvider> 

UserJourneys

В UserJourneys пользователя указываются явные пути, через которые политика позволяет приложению, основанному на утверждениях, предоставлять пользователю требуемые утверждения.

Ниже я добавил пару простых вещей, остальные найти легко в туториалах которые я добавлю ниже.

  <UserJourneys>     <UserJourney Id="SignUp"> <!-- Объявляем ID UserJourney. На него мы будем ссылаться из файлов, которые были загружены после TrustFrameworkExtension.xml-->       <OrchestrationSteps><!-- Создаем шаги. Обязательно соблюдать нумерацию-->         <OrchestrationStep Order="1" Type="ClaimsExchange" ContentDefinitionReferenceId="api.localaccountsignup">           <ClaimsExchanges> <!-- Говорим какую страницу показать задав параметр TechnicalProfileReferenceId. Id нужен для того, что бы обмениваться данными из поставщика -->             <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail-2" />           </ClaimsExchanges>         </OrchestrationStep>         <OrchestrationStep Order="2" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" /> <!-- Отправляем данные на сервер -->       </OrchestrationSteps>       <ClientDefinition ReferenceId="DefaultWeb" />     </UserJourney>     <UserJourney Id="PasswordReset">       <OrchestrationSteps>         <OrchestrationStep Order="1" Type="ClaimsExchange">           <ClaimsExchanges>             <ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />           </ClaimsExchanges>         </OrchestrationStep>         <OrchestrationStep Order="2" Type="ClaimsExchange">           <ClaimsExchanges>             <ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />           </ClaimsExchanges>         </OrchestrationStep>         <OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />       </OrchestrationSteps>       <ClientDefinition ReferenceId="DefaultWeb" />     </UserJourney>        ...   </UserJourneys> 

Пример обмена ClaimsExchange

Ниже пример взаимодействия ClaimsExchanges с поставщиком.

    <ClaimsProviderSelections>       <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />       <ClaimsProviderSelection TargetClaimsExchangeId="GoogleExchange" />       <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />     </ClaimsProviderSelections>     <ClaimsExchanges>       <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />     </ClaimsExchanges>   

Типовые задачи

В результате прочитанного выше вам будет проще понимать туториалы ниже.

SignUpOrSignin.XML, ProfileEdit.XML, PasswordReset.XML

Это конечные файлы, где можно перезаписать \ добавить BuildingBlocks и где мы указываем, какие данные добавить в токен.

  <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xmlns:xsd="http://www.w3.org/2001/XMLSchema"    xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" TenantId="antekesd.onmicrosoft.com" PolicyId="B2C_1A_signup_signin" PublicPolicyUri="http://antekesd.onmicrosoft.com/B2C_1A_signup_signin">    <BasePolicy>     <TenantId>antekesd.onmicrosoft.com</TenantId>     <PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>   </BasePolicy>   <BuildingBlocks>     <ContentDefinitions>       <ContentDefinition Id="api.signuporsignin"> <!-- Перезаписываем внешний вид именно для этой страницы -->         <LoadUri>https://some.blob.core.windows.net/some/some.html</LoadUri>         <RecoveryUri>~/common/default_page_error.html</RecoveryUri>         <DataUri>urn:com:microsoft:aad:b2c:elements:contract:unifiedssp:1.2.0</DataUri>       </ContentDefinition>     </ContentDefinitions>   </BuildingBlocks>   <RelyingParty>     <DefaultUserJourney ReferenceId="SignUpOrSignIn" /> <!-- Указываем путь, по которому должен пройти пользователь-->     <TechnicalProfile Id="PolicyProfile">       <DisplayName>PolicyProfile</DisplayName>       <Protocol Name="OpenIdConnect" />       <OutputClaims> <!-- Добавляем данные в JWT Token. Эти данные нужно возвращать через технические профайлы -->         <OutputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="email"/>         <OutputClaim ClaimTypeReferenceId="givenName" Required="true"/>         <OutputClaim ClaimTypeReferenceId="surname" Required="true"/>         <OutputClaim ClaimTypeReferenceId="email" />         <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>         <OutputClaim ClaimTypeReferenceId="identityProvider" />         <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />         <OutputClaim ClaimTypeReferenceId="picture" />         <OutputClaim ClaimTypeReferenceId="country" Required="true"/>       </OutputClaims>       <SubjectNamingInfo ClaimType="sub" />     </TechnicalProfile>   </RelyingParty> </TrustFrameworkPolicy> 

Тестирование

Для того что бы протестировать свежие изменения нужно:

  1. Перейти в Identity Experience Framework
  2. Выбрать Policy, который вы хотите протестировать
  3. Нажать «Запустить сейчас»

image

Заключение

В результате вы получите форму авторизации полностью (или почти) удовлетворяющую вашим \ заказчика требованиям.

Спасибо за внимание!

ссылка на оригинал статьи https://habr.com/ru/post/504030/