Русский
Русский
English
Статистика
Реклама

Otp

Freeradius Google Autheticator LDAP Fortigate

05.09.2020 02:04:29 | Автор: admin
Как быть, если двухфакторной аутентификации и хочется, и колется, а денег на аппаратные токены нет и вообще предлагают держаться и хорошего настроения.
Данное решение не является чем-то супероригинальным, скорее микс из разных решений, найденных на просторах интернета.

Итак, дано:


Домен Active Directory.
Пользователи домена, работающие через VPN, как многие нынче.
В роли шлюза VPN выступает Fortigate.
Сохранение пароля для VPN-клиента запрещено политикой безопасности.
Политику Fortinet в отношении собственных токенов менее чем жлобской не назовешь бесплатных токенов аж 10 единиц, остальные по очень некошерной цене. RSASecureID, Duo и им подобные не рассматривал, поскольку хочется опенсорса.

Предварительные требования: хост *nix с установленным freeradius, sssd введен в домен, доменные пользователи могут спокойно на нем аутентифицироваться.
Дополнительные пакеты: shellinabox, figlet, freeeradius-ldap, шрифт rebel.tlf с репозитория https://github.com/xero/figlet-fonts.

В моем примере CentOS 7.8.
Логика работы предполагается такая: при подключении к VPN пользователь должен ввести доменный логин и OTP вместо пароля.

Настройка сервисов:


В /etc/raddb/radiusd.conf меняется только пользователь и группа, от имени которых стартует freeradius, так как сервис radiusd должен уметь читать файлы во всех поддиректориях /home/.

user = rootgroup = root

Чтобы можно было использовать группы в настройках Fortigate, нужно передавать Vendor Specific Attribute. Для этого в директории raddb/policy.d создаю файл со следующим содержимым:

group_authorization {    if (&LDAP-Group[*] == "CN=vpn_admins,OU=vpn-groups,DC=domain,DC=local") {            update reply {                &Fortinet-Group-Name = "vpn_admins" }            update control {                &Auth-Type := PAM                &Reply-Message := "Welcome Admin"                }        }    else {        update reply {        &Reply-Message := "Not authorized for vpn"            }        reject        }}

После установки freeradius-ldap в директории raddb/mods-available создается файл ldap.
Нужно создать символьную ссылку в каталог raddb/mods-enabled.

ln -s /etc/raddb/mods-available/ldap /etc/raddb/mods-enabled/ldap

Привожу его содержимое к такому виду:

ldap {        server = 'domain.local'        identity = 'CN=freerad_user,OU=users,DC=domain,DC=local'        password = "SupeSecretP@ssword"        base_dn = 'dc=domain,dc=local'        sasl {        }        user {                base_dn = "${..base_dn}"                filter = "(sAMAccountname=%{%{Stripped-User-Name}:-%{User-Name}})"                sasl {                }                scope = 'sub'        }        group {                base_dn = "${..base_dn}"                filter = '(objectClass=Group)'                scope = 'sub'                name_attribute = cn                membership_filter = "(|(member=%{control:Ldap-UserDn})(memberUid=%{%{Stripped-User-Name}:-%{User-Name}}))"                membership_attribute = 'memberOf'        }}

В файлах raddb/sites-enabled/default и raddb/sites-enabled/inner-tunnel в секции authorize дописываю имя политики, которая будет использоваться group_authorization. Важный момент имя политики определяется не названием файла в директории policy.d, а директивой внутри файла перед фигурными скобками.
В секции authenticate в этих же файлах нужно раскомментировать строку pam.
В файле clients.conf прописываем параметры, с которыми будет подключаться Fortigate:

client fortigate {    ipaddr = 192.168.1.200    secret = testing123    require_message_authenticator = no    nas_type = other}

Конфигурация модуля pam.d/radiusd:

#%PAM-1.0auth       sufficient   pam_google_authenticator.soauth       include      password-authaccount    required     pam_nologin.soaccount    include      password-authpassword   include      password-authsession    include      password-auth

Дефолтные варианты внедрения связки freeradius с google authenticator предполагают ввод пользователем учетных данных в формате: username/password+OTP.

Представив количество проклятий, которое посыпется на голову, в случае использования дефолтной связки freeradius с Google Authenticator, было принято решение использовать конфигурацию модуля pam так, чтобы проверять только лишь токен Google Authenticator.
При подключении пользователя происходит следующее:

  • Freeradius проверяет наличие пользователя в домене и в определенной группе и, в случае успеха, производится проверка OTP токена.

Все выглядело достаточно удачно до момента, пока я не задумался А как же произвести регистрацию OTP для 300+ пользователей?
Пользователь должен залогиниться на сервер с freeradius и из-под своей учетной записи и запустить приложение Google authenticator, которое и сгенерирует для пользователя QR-код для приложения. Вот тут на помощь и приходит shellinabox в комбинации с .bash_profile.

[root@freeradius ~]# yum install -y shellinabox

Конфигурационный файл демона находится в /etc/sysconfig/shellinabox.
Указываю там порт 443 и можно указать свой сертификат.

[root@freeradius ~]#systemctl enable --now shellinaboxd

Пользователю остается лишь зайти по ссылке, ввести доменные креды и получить QR-код для приложения.

Алгоритм следующий:
  • Пользователь логинится на машину через браузер.
  • Проверяется доменный ли пользователь. Если нет, то никаких действий не предпринимается.
  • Если пользователь доменный, проверяется принадлежность к группе администраторов.
  • Если не админ, проверяется настроен ли Google Autheticator. Если нет, то генерируется QR-код и logout пользователя.
  • Если не админ и Google Authenticator настроен, то просто logout.
  • Если админ, то опять проверка Google Authenticator. Если не настроен, то генерируется QR-код.

Вся логика выполняется с использованием /etc/skel/.bash_profile.

cat /etc/skel/.bash_profile
# .bash_profile# Get the aliases and functionsif [ -f ~/.bashrc ]; then        . ~/.bashrcfi# User specific environment and startup programs# Make several commands available from user shellif [[ -z $(id $USER | grep "admins") || -z $(cat /etc/passwd | grep $USER) ]]  then    [[ ! -d $HOME/bin ]] && mkdir $HOME/bin    [[ ! -f $HOME/bin/id ]] && ln -s /usr/bin/id $HOME/bin/id    [[ ! -f $HOME/bin/google-auth ]] && ln -s /usr/bin/google-authenticator $HOME/bin/google-auth    [[ ! -f $HOME/bin/grep ]] && ln -s /usr/bin/grep $HOME/bin/grep    [[ ! -f $HOME/bin/figlet ]] && ln -s /usr/bin/figlet $HOME/bin/figlet    [[ ! -f $HOME/bin/rebel.tlf ]] && ln -s /usr/share/figlet/rebel.tlf $HOME/bin/rebel.tlf    [[ ! -f $HOME/bin/sleep ]] && ln -s /usr/bin/sleep $HOME/bin/sleep  # Set PATH env to <home user directory>/bin    PATH=$HOME/bin    export PATH  else    PATH=PATH=$PATH:$HOME/.local/bin:$HOME/bin    export PATHfiif [[ -n $(id $USER | grep "domain users") ]]  then    if [[ ! -e $HOME/.google_authenticator ]]      then        if [[ -n $(id $USER | grep "admins") ]]          then            figlet -t -f $HOME/bin/rebel.tlf "Welcome to Company GAuth setup portal"            sleep 1.5            echo "Please, run any of these software on your device, where you would like to setup OTP:Google Autheticator:AppStore - https://apps.apple.com/us/app/google-authenticator/id388497605Play Market - https://play.google.com/stor/apps/details?id=com.google.android.apps.authenticator2&hl=enFreeOTP:AppStore - https://apps.apple.com/us/app/freeotp-authenticator/id872559395Play Market - https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=enAnd prepare to scan QR code."            sleep 5            google-auth -f -t -w 3 -r 3 -R 30 -d -e 1            echo "Congratulations, now you can use an OTP token from application as a password connecting to VPN."          else            figlet -t -f $HOME/bin/rebel.tlf "Welcome to Company GAuth setup portal"            sleep 1.5            echo "Please, run any of these software on your device, where you would like to setup OTP:Google Autheticator:AppStore - https://apps.apple.com/us/app/google-authenticator/id388497605Play Market - https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=enFreeOTP:AppStore - https://apps.apple.com/us/app/freeotp-authenticator/id872559395Play Market - https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=enAnd prepare to scan QR code."            sleep 5            google-auth -f -t -w 3 -r 3 -R 30 -d -e 1            echo "Congratulations, now you can use an OTP token from application as a password to VPN."            logout        fi      else        echo "You have already setup a Google Authenticator"        if [[ -z $(id $USER | grep "admins") ]]          then          logout        fi    fi  else    echo "You don't need to set up a Google Authenticator"fi


Настройка Fortigate:


  • Создаем Radius-сервер

  • Создаем необходимые группы, в случае необходимости разграничения доступа по группам. Имя группы на Fortigate должно соответствовать группе, которая передается в Vendor Specific Attribute Fortinet-Group-Name.

  • Редактируем необходимые SSL-порталы.

  • Добавляем группы в политики.



Плюсы данного решения:
  • Есть возможность аутентификации по OTP на Fortigate опенсорс решением.
  • Исключается ввод доменного пароля пользователем при подключении по VPN, что несколько упрощает процесс подключения. 6-цифровой пароль ввести проще, чем тот, который предусмотрен политикой безопасности. Как следствие, уменьшается количество тикетов с темой: Не могу подключиться к VPN.


P.S. В планах докрутить это решение до полноценной 2-хфакторной авторизации с challenge-response.
Подробнее..

Multifactor российская система многофакторной аутентификации

09.11.2020 10:07:58 | Автор: admin

Введение

Долгое время считалось, что классический метод аутентификации, основанный на комбинации логина и пароля, весьма надёжен. Однако сейчас утверждать такое уже не представляется возможным. Всё дело в человеческом факторе и наличии у злоумышленников больших возможностей по угону пароля. Не секрет, что люди редко используют сложные пароли, не говоря уже о том, чтобы регулярно их менять. К сожалению, является типичной ситуация, когда для различных сервисов и ресурсов применяется один и тот же пароль. Таким образом, если последний будет подобран посредством брутфорса или украден с помощью фишинговой атаки, то у злоумышленника появится доступ ко всем ресурсам, для которых применялся этот пароль. Для решения описанной проблемы можно использовать дополнительный фактор проверки личности. Решения, основанные на таком методе, называются системами двухфакторной аутентификации (two-factor authentication, 2FA) или многофакторной аутентификации (multi-factor authentication, MFA). Одним из таких решений является Multifactor от компании Мультифактор. Эта система позволяет выбрать в качестве второго фактора один из следующих инструментов: аппаратный токен, SMS-сообщения, звонки, биометрию, UTF, Google Authenticator, Яндекс.Ключ, Telegram или мобильное приложение. Необходимо добавить, что данное решение предлагается только в качестве сервиса, когда у заказчика устанавливаются лишь программные агенты, а ядро системы размещается на стороне вендора, избавляя таким образом специалистов заказчика от проблем с внесением изменений в инфраструктуру и решением вопросов по организации канала связи с провайдерами для приёма звонков и SMS-сообщений.

Функциональные возможности системы Multifactor

Система Multifactor обладает следующими ключевыми функциональными особенностями:

  • Большой выбор способов аутентификации: Telegram, биометрия, U2F, FIDO, OTP, Google Authenticator, Яндекс.Ключ, мобильное приложение Multifactor, звонки и SMS-сообщения.

  • Предоставление API для управления пользователями из внешних систем.

  • Журналирование действий пользователей при получении доступа.

  • Управление ресурсами, к которым осуществляется доступ.

  • Управление пользователями из консоли администрирования.

  • Возможность импорта пользователей из файлов формата CSV или простого текстового файла.

  • Большой перечень ресурсов, с которыми возможна интеграция Multifactor: OpenVPN, Linux SSH, Linux SUDO, Windows VPN, Windows Remote Desktop, Cisco VPN, FortiGate VPN, Check Point VPN, VMware vCloud, VMware Horizon, VMware AirWatch, Citrix VDI, Huawei.Cloud (в России SberCloud), Outlook Web Access, и другие.

  • Управление функциями системы Multifactor через единую консоль администратора.

  • Информирование администратора системы о потенциальных инцидентах в сфере ИБ.

  • Поддержка Active Directory и RADIUS. Возможность возвращать атрибуты на основе членства пользователя в группе.

Архитектура системы Multifactor

Как уже было сказано, Multifactor является сервисным продуктом. Таким образом, вычислительные мощности и сетевая инфраструктура, необходимые для работы системы, размещены в Москве, в дата-центре Даталайн. ЦОД сертифицирован по стандартам PCI DSS (уровень 1) и ISO/IEC 27001:2005. На стороне заказчика устанавливаются только следующие программные компоненты с открытым исходным кодом:

  • RADIUS Adapter (для приёма запросов по протоколу RADIUS)

  • IIS Adapter (для включения двухфакторной аутентификации в Outlook Web Access)

  • Портал самообслуживания (для самостоятельного управления средствами аутентификации со стороны пользователей).

Системные требования Multifactor

Для корректного функционирования Multifactor производитель установил отдельные системные требования по каждому из компонентов системы. В таблице 1 указаны минимальные ресурсы для RADIUS Adapter.

В таблице 2 приведены показатели, соответствие которым необходимо для установки портала самообслуживания (Self-Service Portal).

Для взаимодействия с большей частью средств коммутации и сервисов в целях осуществления доступа в Multifactor используется сетевой протокол RADIUS (Remote Authentication Dial-In User Service). Система полагается на данный протокол в следующих сценариях:

  • Схема двухфакторной аутентификации, где в качестве первого фактора пользователь применяет пароль, а в качестве второго мобильное приложение, Telegram или одноразовый код (OTP);

  • Схема однофакторной аутентификации, где пользователь применяет логин, а вместо пароля вводится второй фактор (например, пуш-уведомление).

Для того чтобы можно было использовать протокол RADIUS, необходимо обеспечить беспрепятственное подключение устройства доступа (сервер, межсетевой экран или другое средство сетевой коммутации) к адресу radius.multifactor.ru по UDP-порту 1812. Соответственно, данный порт и веб-адрес должны находиться в списке разрешённых.

Кроме того, протокол RADIUS можно применять для обеспечения безопасности подключения по SSH, использования команды SUDO и других операций, требующих усиленного контроля доступа. Также сетевой протокол RADIUS пригодится как дополнительный инструмент проверки подлинности Windows для подключения к удалённому рабочему столу (Remote Desktop).

Для полноценного использования протокола RADIUS в Multifactor применяется программный компонент Multifactor RADIUS Adapter. Multifactor RADIUS Adapter реализует следующие возможности:

  • получение запросов для прохождения аутентификации по протоколу RADIUS;

  • проверка логина и пароля пользователя в Active Directory или NSP (Microsoft Network Policy Server);

  • проверка второго фактора аутентификации на мобильном устройстве пользователя;

  • настройка доступа на основе принадлежности пользователя к группе в Active Directory;

  • включение второго фактора на основе принадлежности пользователя к группе в Active Directory;

  • применение мобильного телефона пользователя из Active Directory для отправки одноразового кода через SMS.

Помимо RADIUS в Multifactor также используется протокол взаимодействия SAML, который кроме двухфакторной аутентификации предоставляет технологию единого входа (SSO) в корпоративные и облачные приложения, где первым фактором может быть логин и пароль от учётной записи в Active Directory либо в Google или Yandex. При использовании протокола SAML в Multifactor можно настроить взаимодействие для аутентификации со следующими приложениями и сервисами: VMware, Yandex.Cloud, SberCloud, Salesforce, Trello, Jira, Slack и др.

Если вас заинтересовал данный продукт, то мы готовы провести совместное тестирование. Следите за обновлениями в наших каналах (Telegram,Facebook,VK,TS Solution Blog)!

Подробнее..

Перевод Как работает single sign-on (технология единого входа)?

17.06.2021 16:13:58 | Автор: admin

Что такое single sign-on?


Технология единого входа (Single sign-on SSO) метод аутентификации, который позволяет пользователям безопасно аутентифицироваться сразу в нескольких приложениях и сайтах, используя один набор учетных данных.


Как работает SSO?


SSO базируется на настройке доверительных отношений между приложением, известным как провайдер услуг, и системой управления доступами, например, OneLogin. Такие доверительные отношения часто базируются на обмене сертификатом между системой управления доступами и провайдером услуг. Такой сертификат может использоваться, чтобы обозначить идентификационную информацию, которая отправляется от системы управления доступами провайдеру услуг, таким образом провайдер услуг будет знать, что информация поступает из надежного источника. В SSO идентификационные данные принимают форму токенов, содержащих идентификационные значения информации о пользователе такие, как email или имя пользователя.


Порядок авторизации обычно выглядит следующим образом:


  1. Пользователь заходит в приложение или на сайт, доступ к которому он хочет получить, то есть к провайдеру услуг.
  2. Провайдер услуг отправляет токен, содержащий информацию о пользователе (такую как email адрес) системе SSO (так же известной, как система управления доступами), как часть запроса на аутентификацию пользователя.
  3. В первую очередь система управления доступами проверяет был ли пользователь аутентифицирован до этого момента. Если да, она предоставляет пользователю доступ к приложению провайдера услуг, сразу приступая к шагу 5.
  4. Если пользователь не авторизовался, ему будет необходимо это сделать, предоставив идентификационные данные, требуемые системой управления доступами. Это может быть просто логин и пароль или же другие виды аутентификации, например одноразовый пароль (OTP One-Time Password).
  5. Как только система управления доступами одобрит идентификационные данные, она вернет токен провайдеру услуг, подтверждая успешную аутентификацию.
  6. Этот токен проходит сквозь браузер пользователя провайдеру услуг.
  7. Токен, полученный провайдером услуг, подтверждается согласно доверительным отношениям, установленным между провайдером услуг и системой управления доступами во время первоначальной настройки.
  8. Пользователю предоставляется доступ к провайдеру услуг.

image

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


Что такое токен в контексте SSO?


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


Является ли технология SSO безопасной?


Ответом на этот вопрос будет "в зависимости от ситуации".


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


SSO также сокращает количество времени, потраченного на восстановление пароля с помощью службы поддержки. Администраторы могут централизованно контролировать такие факторы, как сложность пароля и многофакторную аутентификацию (MFA). Администраторы также могут быстрее отозвать привилегии на вход в систему, если пользователь покинул организацию.


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


Как внедрить SSO?


Особенности внедрения SSO могут отличаться с учетом того, с каким именно решением SSO вы работаете. Но вне зависимости от способа, вам нужно точно знать какие цели вы преследуете. Убедитесь, что вы ответили на следующие вопросы:


  • С какими типами пользователей вы работаете и какие у них требования?
  • Вы ищете локальное или облачное решение?
  • Возможен ли дальнейший рост выбранной программной платформы вместе с вашей компанией и ее запросами?
  • Какие функции вам необходимы, чтобы убедиться в том, что процесс авторизации проходят только проверенные пользователи? MFA, Adaptive Authentication, Device Trust, IP Address Whitelisting, и т.д?
  • С какими системами вам необходимо интегрироваться?
  • Нужен ли вам доступ к программному интерфейсу приложения (API)?

Что отличает настоящую SSO от хранилища или менеджера паролей?


Важно понимать разницу между SSO (Технологией единого входа) и хранилищем или менеджерами паролей, которые периодически путают с SSO, но в контексте Same Sign-On что означает такой же/одинаковый вход, а не единый вход (Single Sign-On). Говоря о хранилище паролей, у вас может быть один логин и пароль, но их нужно будет вводить каждый раз при переходе в новое приложение или на новый сайт. Такая система попросту хранит ваши идентификационные данные для других приложений и вводит их когда это необходимо. В данном случае между приложением и хранилищем паролей не установлены доверительные отношения.


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


В чем разница между программным обеспечением единого входа и решением SSO?


Изучая доступные варианты единого входа, вы можете увидеть, что их иногда называют программным обеспечением единого входа, а не решением единого входа или провайдером единого входа. Часто разница состоит лишь в том, как позиционируют себя компании. Фрагмент программного обеспечения предполагает локальную установку. Обычно это то, что разработано для определенного набора задач и ничего более. Программный продукт предполагает, что есть возможность расширяться и кастомизировать потенциальные возможности исходного варианта. Провайдер будет отличным вариантом, чтобы обратиться к компании, которая производит или пользуется программным продуктом. Например, OneLogin в качестве провайдера SSO.


Бывают ли разные типы SSO?


Когда мы говорим о едином входе (SSO), используется множество терминов:


  • Federated Identity Management (FIM)
  • OAuth (OAuth 2.0 в настоящее время)
  • OpenID Connect (OIDC)
  • Security Access Markup Language (SAML)
  • Same Sign On (SSO)

На самом деле, SSO это часть более крупной концепции под названием Federated Identity Management, поэтому иногда SSO обозначается, как федеративная SSO. FIM просто относится к доверительным отношениям, созданным между двумя или более доменами или системами управления идентификацией. Система единого входа (SSO) это характеристика/фича, доступная внутри архитектуры FIM.


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


OpenID Connect (OIDC) это уровень аутентификации, наложенный на базу OAuth 2.0, чтобы обеспечить фунциональность SSO.


Security Access Markup Language (SAML) это открытый стандарт, который также разработан для обеспечения функциональности SSO.


image

Система Same Sign On, которую часто обозначают, как SSO, на самом деле, не похожа Single Sign-on, т.к не предполагает наличие доверительных отношений между сторонами, которые проходят аутентификацию. Она более привязана к идентификационным данным, которые дублируются и передаются в другие системы когда это необходимо. Это не так безопасно, как любое из решений единого входа.


Также существуют несколько конкретных систем, которые стоит упомянуть, говоря о платформе SSO: Active Directory, Active Directory Federation Services (ADFS) и Lightweight Directory Access Protocol (LDAP).


Active Directory, который в настоящее время именуется, как Active Directory Directory Services (ADDS) это централизованная служба каталогов Microsoft. Пользователи и ресурсы добавляются в службу каталогов для централизованного управления, а ADDS работает с такими аутентификационными протоколами, как NTLM и Kerberos. Таким образом, пользователи, относящиеся к ADDS могут аутентифицироваться с их устройств и получить доступ к другим системам, интегрированным с ADDS. Это и есть форма SSO.


Active Directory Federation Services (ADFS) это тип управления федеративной идентификацией (Federated Identity Management system), которая также предполагает возможность Single Sign-on. Он также поддерживает SAML и OIDC. ADFS преимущественно используется для установления доверительных отношений между ADDS и другими системами, такими как Azure AD или других служб ADDS.


Протокол LDAP (Lightweight Directory Service Protocol) это стандарт, определяющий способ запроса и организации информационной базы. LDAP позволяет вам централизованно управлять такими ресурсами, как пользователи и системы. LDAP, однако, не определяет порядок авторизации, это означает, что он не устанавливает непосредственный протокол, используемый для аутентификации. Но он часто применяется как часть процесса аутентификации и контроля доступа. Например, прежде, чем пользователь получит доступ к определенному ресурсу, LDAP сможет запросить информацию о пользователе и группах, в которых он состоит, чтобы удостовериться, что у пользователя есть доступ к данному ресурсу. LDAP платформа на подобие OpenLDAP обеспечивает аутентификацию с помощью аутентификационных протоколов (например, Simple Authentication и Security Layer SASL).


Как работает система единого входа как услуга?


SSO функционирует также, как и многие другие приложения, работающие через интернет. Подобные OneLogin платформы, функционирующие через облако, можно отнести к категории решений единого входа Software as a Service (SaaS).


Что такое App-to-App (приложение-приложение) SSO?


В заключение, возможно вы слышали о App-to-App SSO. Пока еще такой подход не является стандартным. Такое понятие больше используется SAPCloud для обозначения процесса передачи идентификационных данных пользователя из одного приложения в любое из других, состоящих в их экосистеме. В какой-то степени такой метод присущ OAuth 2.0, но хочется снова подчеркнуть, что это не стандартный протокол или метод. В настоящее время он является характерным только для SAPCloud.

Подробнее..

Аутентификация в IoT

24.12.2020 00:21:35 | Автор: admin

В последнее время постоянно растет количество умных вещей, таких как камеры, различные датчики, умные лампочки, выключатели и многое другое. Эти вещи имеют постоянный доступ в интернет и активно обмениваются данными для аналитики с приложениями. На самом деле существуют более стратегически важные данные, такие как показание датчиков о здоровье пациентов. Встает очень закономерный вопрос, как защищать данные такого рода, которые в реальном времени(без усмотрения пользователя) отпарвляются на сторонние устройства. Дело в том, что очень непросто спроектировать и построить уникальную и на сто процентов безопасную систему IoT(Internet of things), из-за того что устройства имеют различные операционные системы, цели и масштабы - некоторые работают в пределах вашей комнаты, а некоторые, например, должны охватить систему видеонаблюдения района города.

В этой статье мы рассмотрим, различные механизмы аутентификации - по имени пользователя и паролю, по токену, с помощью OTP(one-time password) и, наконец, сертификатов. Например, на бытовом уровне это позволяет защитить счётчики электроэнергии от неавторизованного доступа, и защитить данные от подмены.

Вернее всего будет начать с весьма фундаментальной вещи для IoT - платформы. Платформа IoT - это инструмент, который объединяет вещи и интернет и по сути является основой построений новых решений в IoT. Рынок платформ очень стремительно растет и рассматривать их все не имеет смысла и к тому же не является целью этой статьи. Поскольку общности это не нарушит, в качестве примера рассмотрим платформу от компании IBM(International Business Machines) .

Чтобы понять, в какие моменты должны срабатывать механизмы аутентификации давайте рассмотрим структуру стандартного IoT-приложения на основе IBM Watson IoT Platform и облачной платформы IBM Bluemix.

На Рис. 1 представлена структура IOT-приложения.На Рис. 1 представлена структура IOT-приложения.

Мы не будем вдаваться в подробности структуры, но если вкратце: устройства публикуют данные датчиков в IBM Watson IoT Platform, которая обменивается с IoT-приложением (на Bluemix) с помощью протокола MQTT - Message Queueing Telemetry Transport Protocol. Далее устройства получают от приложений инструкции по выполнению функций управления.

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

Перед тем, как перейти к аутентификации необходимо ввести понятие брокера - мы встречали его раньше на картинке 1. Если простыми словами брокер - помошник, который работает с потоками данных от устройств, умеет ими управлять и манипулировать, добиваясь максимальной эффективности от использования IoT - устройств. Расширенные возможности обмена сообщениями являются ключевыми для всех экосистем Интернета вещей.

Аутентификация по имени пользователя и паролю

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

После этого брокер отвечает сообщением CONNACK, в которм есть поле Return Code, которое по сути является ответом от сервера об успешной или неуспешной аутентификации. Далее клиент отправляет сообщение SUBSCRIBE брокеру и подписывается на получение сообщений для него.
Чтобы подтвердить подписку Брокер MQTT отправляет сообщение SUBACK обратно клиенту.

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

На Рис. 2 представлена схема взаимодействия устройства и брокера.На Рис. 2 представлена схема взаимодействия устройства и брокера.

Аутентификация по токену доступа

В этом варианте также используется команда CONNECT. Клиент извлекает токен доступа и отправляет его брокеру через поле password. С этим токеном брокер может проверить были ли изменены данные, и не истек ли срок действия токена.

Аутентификация на основе одноразового пароля (OTP)

Такой подход позволяет уберечь устройство от неавторизованного пользователя и от ненадлежащего использования. Когда включен этот механизм, устройство после включения отсылает в IOT-приложение, через брокера OTP-request. Приложение генерирует одноразовый пароль и отсылает его на доверенное устройство владельца, также приложение шлет уведомление на устройство, которое мы хотим авторизовать. Далее пароль вводится в устройство и отсылается приложению, где пароль проверяется. В конфигурации можно задать количество попыток для OTP-аутентификации и если не удалось пройти аутентификацию после повторных попыток, приложение завершает работу.

Аутентификация на основе сертификатов

Не все брокеры поддерживают сертификаты устройств. Этот механизм используется в системах, где требования к безопасности высоки. Вообще говоря MQTT может использовать протокол транпортного уровня модели TCP/IP - TLS(Transport Layer Sequrity), и чтобы лучше понять как происходит установление соединения с помощью протокола TLS, рассмотрим этот процесс.

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

Как происходит соединение? Клиент высылает сообщение Client Hello, в которое вставляется набор шифров TLS, которые он поддерживает и Clent Random - число, которое используется для генерации ключей симметричного шифрования. Затем сервер шлёт сообщение Server Hello, где выбирает шифр TLS, из ранее ему предоставленных от клиента, а также Server Random(аналогично клиенту). Кроме этого сервер создает Идентификатор сесси, по которму позже можно восстановить ссесию не меняя ключи и алгоритмы шифрования, что само по себе хорошо, так как не нужно повторно проводить установку соединения. Далее сервер шлет клиенту сертификат - файл, содержащий ключ сервера. Получив сертификат, клиент его проверяет. Через некоторе время сервер шлет сообщение Server Key Exchange, для обмена ключами(это не обязательное сообщение, зависит от алгоритмов шифрования, которые были выбраны). Затем сервер отсылает Server Hello Done, после которого должен действовать клиент.

Далее идет процедура обмена ключами - клиент шлет Client Key Exchange, где передаёт информацию необходимую для получения разделяемого ключа(эта информация, опять же, зависит от алгоритма обмена ключами). Например в RSA используется открытый ключ сервера, который находился в сертификате, полученном от сервера и клиент генерирует pre-master secret(это предварительный ключ, на основе которого будут получены ключи симметричного шифрования). Используя открытый ключ из сертификата, клиент зашифровывает pre-master secret и отправляет на сервер в сообщении Client Key Exchange. Далее сервер получает этот ключ и с помощью своего закрытого ключа расшифровывает ранее полученное сообщение, таки образом он получает pre-master secret. Таким образом клиент и сервер имеют одинаковые ключи(pre-master secret), на основе которых они рассчитывают ключи для шифрования, используя Client Random и Server Random. Теперб всё готово чтобы переключиться на безопасное соединение и клиент отсылает Change Ciper Spec и Finished(это сообщение уже зашифровано), а сервер отвечает аналогичной парой сообщений.

Теперь все готово для передачи сообщений! А мы возвращаемся к IOT. В нашем слушае необходимо верифицировать еще и клиента! А поэтому перед Server Hello Done сервер запрашивает сертификат клиента. Сервер проверяет сертификат клиента, и если все успешно, то подлинность клиента также подтверждена. Это позволяет аутентифицировать клиента до установления безопасного соединения, что позволяет аутентифицировать клиента на транспортном уровне, а не на уровне приложений, а, следовательно, и до сообщения CONNECT в котором передаётся пароль и имя.

Стоит добавить, что при утечке данных, или каких-либо проблем с устройством, сервер должен сбросить такой сертификат и перестать подключать клиентов с этим сертификатом.

Итоги

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

Источники

1. http://blog.catchpoint.com/2017/05/30/protocol-for-internet-of-things/

2. https://developer.ibm.com/articles/iot-trs-secure-iot-solutions1/

3. https://iot-analytics.com/5-things-know-about-iot-platform/

Подробнее..

Recovery mode Перевозим волка, козу и капусту через реку с эффектами на Elixir

03.08.2020 22:10:54 | Автор: admin

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


Первой ласточкой были Примерно 20 строк для подсчета слов, появившиеся как алаверды на Побеждая C двадцатью строками Haskell: пишем свой wc от 0xd34df00dсегодня же я наткнулся на Перевозим волка, козу и капусту через реку с эффектами на Haskell от iokasimov и тоже не устоял.


Итак, встречайте: ленивый полный асинхронный параллельный перебор против алгебраических эффектов.




Постановка задачи (благодарно скопипащщено из оригинальной заметки):


Однажды крестьянину понадобилось перевезти через реку волка, козу и капусту. У крестьянина есть лодка, в которой может поместиться, кроме самого крестьянина, только один объект или волк, или коза, или капуста. Если крестьянин оставит без присмотра волка с козой, то волк съест козу; если крестьянин оставит без присмотра козу с капустой, коза съест капусту.

Волк Коза Капуста


Мы будем действовать следующим образом: начнем с состояния все на левом берегу, и на каждом шаге будем запускать максимум столько эрланг-процессов, сколько на этом берегу живности (+1 для ходки порожняком). При этом мы всегда будем проверять, что остающаяся на берегу живность друг друга не перегрызет, и эти ветки будем отсекать сразу. Также мы будем хранить историю, и отсекать циклические ветки, возвращающие нас в уже виденное состояние. Это, кстати, не избыточные данныеисторию поездок мы будем возвращать в качестве результата.


Итак, начнем с объявлений.


defmodule WolfGoatCabbage.State do  @moduledoc """  Текущее состояние нашей микровселенной.  Берега (`true` исходный, левый), `ltr`маркер направления, история поездок.  """  defstruct banks: %{true => [], false => []}, ltr: true, history: []enddefmodule WolfGoatCabbage.Subj do  @moduledoc """  Единица живности, с кем конфликтует.  """  defstruct [:me, :incompatible]end

Поскольку парсер хабра все еще живет в XIX веке, вот вам картинка с начальными значениями.


Начальные значения


Что ж, можно и перейти к собственно написанию кода.


Проверка целостности


@spec safe?(  banks :: %{true => [%Subj{}], false => [%Subj{}]},  ltr :: boolean()) :: boolean()defp safe?(banks, ltr) do  subjs =    banks[ltr]    |> Enum.map(& &1.me)    |> MapSet.new()  incompatibles =    banks[ltr]    |> Enum.flat_map(& &1.incompatible)    |> MapSet.new()  MapSet.disjoint?(subjs, incompatibles)end

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


Ход (лодкой)


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


@spec move(%State{}, nil | %Subj{}) :: %State{} | false@doc """Если в лодке никого, достаточно проверить, что мы не оставляем берег в уже виденном состоянии, и напрямую вернуть новое состояние."""defp move(%State{ltr: ltr, banks: banks, history: history} = state, nil) do  !(ltr || Enum.member?(history, MapSet.new(banks[ltr]))) &&    %State{state | ltr: not ltr, history: [length(history) | history]}end@doc """Когда в лодке блеют, тявкают, или выразительно хлопают листьями все немного сложнее.Мы переносим живность с одного берега на другой, удостоверяемся, чтоходка безопасна (на покидаемом берегу не возникнет внеплановый ужин) ичто мы еще не видели такого состояния. Если все критерии выполненывозвращаем новое состояние, если неттерминирующий `false`."""defp move(%State{banks: banks, ltr: ltr, history: history}, who) do  with banks <- %{ltr => banks[ltr] -- [who], not ltr => [who | banks[not ltr]]},        true <- safe?(banks, ltr),        true <- not Enum.member?(history, MapSet.new(banks[true])) do    %State{      banks: banks,      ltr: not ltr,      history: [MapSet.new(banks[true]) | history]    }  endend

Собственно партия (многоходовочка)


Осталось, собственно, написать основную часть: рекурсивный запуск процессов. Нет ничего проще.


@initial %State{            banks: %{true => @subjs, false => []},            history: [MapSet.new(@subjs)]         }@spec go(%State{}) :: [MapSet.t()]def go(state \\ @initial) do  case state.banks[true] do    [] -> # ура!      Enum.reverse(state.history)    some ->      [nil | some]      |> Task.async_stream(&move(state, &1))      |> Stream.map(&elem(&1, 1)) # лениво      |> Stream.filter(& &1)      # лениво      |> Stream.flat_map(&go/1)   # лениво и рекурсивно  endend

Спасибо Stream, весь этот код ленив, сиречь выполняться не будет, пока не пнут. Мы же тут хаскель пародируем, помните?


Проверяем


Тесты я недолюбливаю и считаю пустой тратой времени: гораздо проще сразу писать рабочий код. Поэтому я просто создам функцию main/0 и выведу результаты на экран.


Тут есть один нюанс: несколько решений вернутся плоским списком из-за Stream.flat_map/2. Но это не страшно: каждое решение заканчивается пустым множеством, поэтому мы легко разобьем этот плоский лист на чанки. Весь код красивого вывода (которого чуть ли не столько же, сколько логики) я тут приводить не буду, вот gist для энтузиастов.




Удачной сельскохозяйственной перевозки!

Подробнее..

To spawn, or not to spawn?

11.04.2021 18:19:06 | Автор: admin

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

Но перед тем, как начать, т. к. статья будет длинной, я хотел бы обозначить основные моменты:

  • Используйте функции и модули для разделения мыслительных сущностей.

  • Используйте процессы для разделения сущностей времени выполнения.

  • Не используйте процессы (даже агентов) для разделения сущностей мышления.

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

Использование для этого процессов (например агентов) - это ошибка, которую люди часто допускают. Такой подход существенно упускает функциональную составляющую Elixir и вместо этого пытается имитировать объекты процессами. Реализация, скорее всего, будет хуже, чем простой функциональный подход (или даже эквивалент на языке объектно-ориентированного программирования). Поэтому стоит обращаться к процессам, только когда есть ощутимые выгоды от этого. Организация кода не входит в число этих преимуществ, так что это не лучший повод для использования процессов.

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

Пример

Но как же тогда управлять сложным состоянием, если не с помощью агентов и процессов? Позвольте мне проиллюстрировать эту идею на простой предметной модели сокращенной и слегка модифицированной версии игры в блэкджек. Код, который я вам покажу (доступен здесь), запускает один раунд на столе для блэкджека.

Раунд - это, по сути, последовательность раздач, каждая из которых принадлежит отдельному игроку. Раунд начинается с первой руки. Игроку сначала выдают две карты, а затем он делает ход: берет еще одну карту (хит) или останавливается (стоп). В первом случае игроку дается еще одна карта. Если счет игрока больше 21, игрок вылетает из игры. В противном случае игрок может сделать еще один ход (взять карту или остановиться).

Счет руки - это сумма всех достоинств карт, при этом числовые карты (2-10) имеют свои соответствующие значения, а валет, дама и король имеют значение 10. Туз может быть оценен как 1 или как 11, в зависимости от того, что дает лучший (но не проигранный) счет.

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

Для простоты я не рассматривал такие понятия, как дилер, ставки, страхование, разделение(), несколько раундов, люди, присоединяющиеся к столу или покидающие его.

Границы процесса

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

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

Итак, в этом случае я вижу много потенциальных недостатков и не очень много преимуществ от использования нескольких процессов для управления состоянием одного раунда. Однако разные раунды взаимно независимы. У них есть свои отдельные потоки, они держат свои отдельные состояния, у них нет ничего общего. Таким образом, управление несколькими раундами в одном процессе контрпродуктивно. Это увеличит нашу поверхность ошибок (сбой в одном раунде приведет к отключению всего) и, возможно, приведет к снижению производительности (мы не используем несколько ядер) или узким местам (длительная обработка в одном раунде парализует все остальные). Если мы будем запускать разные раунды в разных процессах, есть очевидные выигрыши, так что это решение не составит труда :-) (прим. переводчика: Как же нет ничего общего, а состояние кошельков игроков?)

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

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

Функциональное моделирование

Итак, как же мы можем разделить разные сущности без использования нескольких процессов? Конечно, используя функции и модули. Если мы распределим разные части логики по разным функциям, дадим этим функциям собственные имена и, возможно, организуем их в правильно названные модули, мы сможем прекрасно представить наши идеи без необходимости имитировать объекты с помощью агентов.

Позвольте мне показать вам, что я имею в виду, проведя вас через каждую часть моего решения, начиная с самого простого.

Колода карт

Первое, что я хочу запечатлеть - это колода карт. Мы хотим смоделировать стандартную колоду из 52 карт. Мы хотим начать с перетасованной колоды, а затем иметь возможность брать из нее карты одну за другой.

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

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

@cards (  for suit <- [:spades, :hearts, :diamonds, :clubs],      rank <- [2, 3, 4, 5, 6, 7, 8, 9, 10, :jack, :queen, :king, :ace],    do: %{suit: suit, rank: rank})

Теперь я могу добавить функцию shuffle/0 для создания перемешанной колоды:

def shuffled(), do:  Enum.shuffle(@cards)

И наконец, take/1, которая берёт верхнюю карту из колоды:

def take([card | rest]), do:  {:ok, card, rest}def take([]), do:  {:error, :empty}

Функция take/1 возвращает либо {:ok, card_taken, rest_of_the_deck}, либо {:error, :empty}. Такой интерфейс заставляет клиента (пользователя абстракции колоды) явно решать, как поступать в каждом случае.

Как мы можем это использовать:

deck = Blackjack.Deck.shuffled()case Blackjack.Deck.take(deck) do  {:ok, card, transformed_deck} ->    # do something with the card and the transform deck  {:error, :empty} ->    # deck is empty -> do something elseend

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

  • кучи связанных функций,

  • с описательными именами,

  • которые не проявляют побочных эффектов,

  • и могут быть извлечены в отдельный модуль

Для меня это то, что соответствует классам и объектам в объектно-ориентированном пространстве. В объектно-ориентированном языке у меня может быть класс Deck с соответствующими методами, здесь у меня есть модуль Deck с соответствующими функциями. Предпочтительно (хотя и не всегда стоит затраченных усилий), чтобы функции только преобразовывали данные, не имея дело с темпоральной логикой или побочными эффектами (обмен сообщениями между процессами, база данных, сетевые запросы, тайм-ауты и т. д.)

Не так важно, находятся ли эти функции в выделенном модуле. Код этой абстракции довольно прост и используется только в одном месте. Поэтому я мог бы также определить приватные функции shuffled_deck/0 и take_card/1 в клиентском модуле. Фактически, это то, что я часто делаю, если код достаточно мал. Я всегда могу выделить это позже, если что-то усложнится. (прим. переводчика: не совсем уловил здесь мысль, которую хотел донести автор)

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

Полный код модуля доступен здесь.

Рука

Эту же технику можно использовать для управления рукой. Эта абстракция отслеживает карты в руке. Она также умеет подсчитывать очки и определять статус руки (:ok или :busted). Реализация находится в модуле Blackjack.Hand.

Модуль выполняет две функции. Мы используем new/0 для создания экземпляра руки, а затем deal/2, чтобы раздать карту руке. Вот пример комбинации руки и колоды:

# create a deckdeck = Blackjack.Deck.shuffled()# create a handhand = Blackjack.Hand.new()# draw one card from the deck{:ok, card, deck} = Blackjack.Deck.take(deck)# give the card to the handresult = Blackjack.Hand.deal(hand, card)

Результат deal/2 вернётся в форме {hand_status, transformed_hand}, где hand_status это или :ok или :busted.

Раунд

Эта абстракция, реализованная в модуле Blackjack.Round, связывает всё воедино. Она имеет следующие обязанности:

  • сохранять состояния колоды

  • держать состояние всех рук в раунде

  • решать, кому переходит следующий ход

  • получать и интерпретировать ход игрока (хит / стоп)

  • брать карты из колоды и передавать их текущей руке

  • вычислять победителя после того, как все руки разыграны

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

У меня сложилось впечатление, что многие люди, включая опытных эрлангистов/эликсирщиков, реализовали бы концепцию раунда непосредственно в GenServer или в :gen_statem. Это позволит им управлять состоянием раунда и темпоральной логикой (например, общением с игроками) в одном месте.

Однако я считаю, что эти два аспекта необходимо разделить, поскольку оба они потенциально сложны. Логика одного раунда уже в некоторой степени запутана, и она может только ухудшиться, если мы захотим поддержать дополнительные аспекты игры, такие как ставки, сплиты или раздающего игрока. Общение с игроками имеет свои проблемы, если мы хотим иметь дело с расщеплениями сети(netsplits), сбоями, медленными или недоступными клиентами. В этих случаях нам может потребоваться поддержка повторных попыток, возможно, добавить некоторую персистентность, event sourcing или что-то еще.

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

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

Позвольте показать вам код. Чтобы создать новый раунд, мне нужно вызвать start/1:

{instructions, round} = Blackjack.Round.start([:player_1, :player_2])

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

  • создание руки для каждого игрока

  • отслеживание текущего игрока

  • отправка уведомлений игрокам

    Функция возвращает кортеж. Первый элемент кортежа - это список инструкций. Пример:

    [{:notify_player, :player_1, {:deal_card, %{rank: 4, suit: :hearts}}},{:notify_player, :player_1, {:deal_card, %{rank: 8, suit: :diamonds}}},{:notify_player, :player_1, :move}]
    

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

  • уведомить игрока 1, что он получил четвёрку червей

  • уведомить игрока 1, что он получил восьмёрку бубён

  • уведомить игрока 1, что ему нужно сделать ход

    Фактическая доставка этих уведомлений заинтересованным игрокам является ответственностью клиентского кода. Клиентским кодом может быть, скажем, GenServer, который будет отправлять сообщения процессам игроков. Он также будет ждать, пока игроки не сообщат, когда они захотят взаимодействовать с игрой. Это временная(темпоральная) логика, и она полностью хранится за пределами модуля Round.

Второй элемент возвращённого кортежа, называется round, это состояние самого раунда. Стоит отметить, что эти данные типизированы как непрозрачные. Это значит, что клиент не может читать эти данные внутри переменной round. Всё, что нужно клиенту, будет доставлено в списке instruction.

Давайте продвинемся на шаг вперед в этом раунде, взяв следующую карту игроком 1:

{instructions, round} = Blackjack.Round.move(round, :player_1, :hit)

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

Вот инструкции, которые я получил:

[  {:notify_player, :player_1, {:deal_card, %{rank: 10, suit: :spades}}},  {:notify_player, :player_1, :busted},  {:notify_player, :player_2, {:deal_card, %{rank: :ace, suit: :spades}}},  {:notify_player, :player_2, {:deal_card, %{rank: :jack, suit: :spades}}},  {:notify_player, :player_2, :move}]

Этот список говорит мне, что игрок 1 получил десятку пик. Поскольку раньше у него было 4 червы и 8 бубён, игрок вылетает из игры, и раунд немедленно переходит к следующей руке. Клиенту предлагается уведомить игрока 2 о том, что у него есть две карты, и что он должен сделать ход.

Сделаем ход от имени игрока 2:

{instructions, round} = Blackjack.Round.move(round, :player_2, :stand)# instructions:[  {:notify_player, :player_1, {:winners, [:player_2]}}  {:notify_player, :player_2, {:winners, [:player_2]}}]

Игрок 2 не взял другую карту, поэтому его рука завершена. Абстракция немедленно определяет победителя и инструктирует нас проинформировать обоих игроков о результате.

Давайте посмотрим, как Round прекрасно сочетается с абстракциями Deck и Hand. Следующая функция из модуля Round берет карту из колоды и передает ее текущей руке:

defp deal(round) do  {:ok, card, deck} =    with {:error, :empty} <- Blackjack.Deck.take(round.deck), do:      Blackjack.Deck.take(Blackjack.Deck.shuffled())  {hand_status, hand} = Hand.deal(round.current_hand, card)  round =    %Round{round | deck: deck, current_hand: hand}    |> notify_player(round.current_player_id, {:deal_card, card})  {hand_status, round}end

Берём карту из колоды, используя новую колоду, если текущая закончилась. Затем мы передаем карту в текущую руку, обновляем раунд новой рукой и статусом колоды, добавляем инструкцию по уведомлению о данной карте и возвращаем статус руки (:ok или :busted) и обновленный раунд. Никаких дополнительных процессов в этом процессе не задействовано :-)

Вызов notify_player - это простой однострочник, который избавляет этот модуль от многих сложностей. Без него нам нужно было бы отправить сообщение другому процессу (например, другому GenServer или каналу Phoenix). Пришлось бы как-то найти этот процесс и рассмотреть случаи, когда этот процесс не запущен. Вместе с кодом, который моделирует ход раунда, пришлось бы связать много дополнительных сложностей.

Но благодаря механизму инструкций, ничего из этого не случилось, и модуль Round остался сфокусированным на правилах игры. Функция notify_player будет сохранять инструкцию. Позже, перед выходом, функция take_instructions из Round будет забирать все ожидающие инструкции, и возвращать их по отдельности, вынуждая клиентский код интерпретировать их.

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

Организация процесса

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

Сервер раунда

Каждый раунд управляется модулем Blackjack.RoundServer, который есть GenServer. Agent также мог бы подойти для этих целей, но я не фанат агентов, так что я остановлюсь на GenServer. Ваши предпочтения могут отличаться, конечно, и я полностью уважаю ваше мнение :-)

Чтобы запустить процесс, нам нужно вызвать функцию start_playing/2. Это имя выбрано вместо более распространенного start_link, поскольку start_link по соглашению ссылается на вызывающий процесс. Напротив, start_playing начнет раунд где-то еще в дереве надзора, и процесс не будет связан с вызывающим.

Функция принимает два аргумента: идентификатор раунда и список игроков. Идентификатор раунда - это произвольный уникальный терм, который должен быть выбран клиентом. Серверный процесс будет зарегистрирован во внутреннем реестре с использованием этого идентификатора.

Каждая запись в списке игроков представляет собой словарь, описывающий клиентскую часть игрока:

@type player :: %{id: Round.player_id, callback_mod: module, callback_arg: any}

Игрок описывается его идентификатором, модулем обратного вызова и аргументом обратного вызова. Идентификатор будет передан абстракции раунда. Всякий раз, когда абстракция инструктирует сервер уведомить некоторого игрока, сервер вызывает callback_mod.some_function (some_arguments), где some_arguments будет включать идентификатор раунда, идентификатор игрока, callback_arg и дополнительные аргументы, специфичные для уведомления.

Подход callback_mod позволяет нам поддерживать различные типы игроков, такие как:

  • игроков, подключенных через HTTP

  • игроков, подключенных через настраиваемый протокол TCP

  • игрок в сеансе оболочки iex

  • автоматических игроков (ботов)

    Мы легко справимся со всеми этими игроками в одном раунде. Сервер не заботится ни о чём из этого, он просто вызывает функции обратного вызова модуля обратного вызова и позволяет реализации выполнять работу.

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

@callback deal_card(RoundServer.callback_arg, Round.player_id,  Blackjack.Deck.card) :: any@callback move(RoundServer.callback_arg, Round.player_id) :: any@callback busted(RoundServer.callback_arg, Round.player_id) :: any@callback winners(RoundServer.callback_arg, Round.player_id, [Round.player_id])  :: any@callback unauthorized_move(RoundServer.callback_arg, Round.player_id) :: any

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

Другое приятное следствие такого дизайна - это то, что тестирование этого сервера довольно просто. Тест реализует уведомления путём отправки сообщений самому себе из каждого колбека. Затем тестирование сводится к asserting/refuting определённых сообщений, и вызову RoundServer.move/3, чтобы сделать ход от имени игрока.

Отправка сообщений

Когда функция модуля Round возвращает список инструкций серверному процессу, тот пройдёт по этому списку, и интерпретирует инструкции.

Сами уведомления отправляются из отдельных процессов. Это пример, в котором мы можем получить преимущество от дополнительного параллелизма. Отправка уведомлений - это задача, отдельная от задачи управления состоянием раунда. Логика уведомлений может быть перегружена такими задачами, как медленные или отключенные клиенты, поэтому стоит делать это вне раунда. Более того, уведомления разным игрокам не имеют ничего общего, поэтому их можно отправлять из разных процессов. Однако нам нужно сохранить порядок уведомлений для каждого игрока, поэтому нам нужен отдельный процесс уведомления для каждого игрока.

Это реализовано в модуле Blackjack.PlayerNotifier, процессе на основе GenServer, чья роль - отправлять уведомление отдельному игроку. Когда мы стартуем сервер раунда функцией start_playing/2, запускается небольшое поддерево надзора в котором размещается сервер раунда вместе с одним сервером уведомлений на каждого игрока в раунде.

Когда сервер раунда делает ход, он получает список инструкций от абстракции раунда. Затем он перенаправляет каждую инструкцию соответствующему серверу уведомлений, который интерпретирует эту инструкцию и вызывает соответствующий модуль/функцию/аргументы(M/F/A) для уведомления игрока.

Следовательно, если нам нужно уведомить нескольких игроков, мы сделаем это отдельно (и, возможно, параллельно). Как следствие, общий порядок сообщений не сохраняется. Рассмотрим следующую последовательность инструкций:

[  {:notify_player, :player_1, {:deal_card, %{rank: 10, suit: :spades}}},  {:notify_player, :player_1, :busted},  {:notify_player, :player_2, {:deal_card, %{rank: :ace, suit: :spades}}},  {:notify_player, :player_2, {:deal_card, %{rank: :jack, suit: :spades}}},  {:notify_player, :player_2, :move}]

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

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

Сервис блэкджека

Картинка завершается в виде приложения OTP :blackjack (модуль Blackjack). Когда вы запускаете приложение, запускается пара локально зарегистрированных процессов: экземпляр внутреннего реестра Registry (используется для регистрации серверов раунда и уведомлений) и супервизор :simple_one_for_one, который будет размещать поддерево процесса для каждого раунда.

Это приложение теперь в основном представляет собой сервис блэкджека, который может управлять несколькими раундами. Сервис является универсальным и не зависит от конкретного интерфейса. Вы можете использовать его с Phoenix, Cowboy, Ranch (для простого TCP), elli или любым другим, подходящим для ваших целей. Вы реализуете модуль обратного вызова, запускаете клиентские процессы и запускаете сервер раунда.

Вы можете посмотреть примеры в модуле Demo, который реализует простого автоигрока, модуль обратного вызова сервиса уведомлений, основанного на GenServer, и логику старта, которая стартует раунд с пятью игроками:

$ iex -S mixiex(1)> Demo.runplayer_1: 4 of spadesplayer_1: 3 of heartsplayer_1: thinking ...player_1: hitplayer_1: 8 of spadesplayer_1: thinking ...player_1: standplayer_2: 10 of diamondsplayer_2: 3 of spadesplayer_2: thinking ...player_2: hitplayer_2: 3 of diamondsplayer_2: thinking ...player_2: hitplayer_2: king of spadesplayer_2: busted...

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

Заключение

Итак, можем ли мы управлять сложным состоянием в одном процессе? Конечно, можем! Простые функциональные абстракции, такие как Deck and Hand, позволили мне разделить проблемы более сложного состояния раунда без необходимости прибегать к помощи агентов.

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

Если временная логика и/или логика предметной области сложны, рассмотрите возможность их разделения. Подход, который я использовал, позволил мне реализовать более сложное поведение во время выполнения (одновременные уведомления), не усложняя бизнес-процесс раунда. Это разделение также ставит меня в удобное положение, поскольку теперь я могу развивать оба аспекта по отдельности. Добавление поддержки бизнес-концепций дилера, сплита, страхования и других не должно существенно влиять на аспект выполнения. Точно так же поддержка расщеплений сети(netsplits), повторных подключений, сбоев игрока или тайм-аутов не должна требовать изменений в логике домена.

Наконец, стоит помнить о конечной цели. Хотя я туда не ходил (пока), я всегда планировал, что этот код будет размещен на каком-то веб-сервере. Так что некоторые решения принимаются в поддержку этого сценария. В частности, реализация RoundServer, которая принимает модуль обратного вызова для каждого игрока, позволяет мне подключаться к различным типам клиентов, использующим различные технологии. Это делает сервис блэкджека независимым от конкретных библиотек и фреймворков (за исключением стандартных библиотек и OTP, конечно) и делает его полностью гибким.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru