Hydra

35 minute read

Перевод статьи "Hydra: Run your own Identity and Access Management service in <5 Minutes".

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

Hydra появилась на свет благодаря острой необходимости в масштабируемом 12factor приложении для OAuth сервисе корпоративного уровня и, при этом, без всяких сумасшедших фич и тонны сторонних зависимостей. Кроме того, мы добавили политики, управление аккаунтами/клиентами и еще кое-какие фичи. Мы решили не реализовывать поддержку 5 различных баз данных, что позволило сократить дерево зависимостей и теперь Hydra работает только с PostgreSQL и другими SQL'ными базами данных.

Краткое описание основных возможностей Hydra:

  • Управление аккаунтами: вход пользователей, настройки, востановление пароля
  • Контроль доступа/Реализация политики/Хранение политик реализованные через Ladon.
  • Богатый набор различных фич OAuth2:
    • Hydra реализует протокол OAuth2 согласно спецификациям rfc6749 и draft-ietf-oauth-v2-10 с использованием using osin и osin-storage.
    • Hydra использует автономные токены доступа согласно rfc6794#section-1.4 через эммисионные JSON веб-токены, описанные в rfc7519 с RSASSA-PKCS1-v1_5 SHA-256 шифрованием.
    • Hydra реализует OAuth2 Introspection (rfc7662) и OAuth2 Revokation (rfc7009)
    • Hydra предоставляет возможность для входа через сторонние провайдеры, такие как ropbox, LinkedIn, Google и т.д.
  • Hydra не показывает никакого HTML. И нам кажется, что это правильный подход. Hydra предоставляет только базовый функционал.
  • В комплекте есть очень простые консольные утилиты, такие как hydra-host jwt для генерации jwt подписи к ключу/значению или hydra-host client create.
  • Hydra работает и по HTTP/2 с TLS, и по HTTP(не забывайте, что это не очень безопасно).
  • Для Hydra написаны много различных тестов. Мы используем github.com/ory-am/dockertest для поднятия Postgres(и остальных зависимостей) на лету и запуска интеграционных тестов. Можете попробовать такой же подход для ускорения своих интеграционных тестов.

Hydra написан мной (GitHub/LinkedIn) как часть бизнес приложения, которое пока в разработке. Сейчас Hydra поддерживает Thomas Aidan Curran.

Почему Hydra это бекенд?

Hydra не предоставляет HTML страницы для логина, разлогирования или авторизации. Вместо это, если необходимо выполнить некоторое действие, то Hydra редиректит на заранее определенный URL, к примеру http://sign-up-app.yourservice.com/sign-up или http://sign-in-app.yourservice.com/sign-in. К тому же, пользователь может использовать другой OAuth2 провайдер, например Dropbox или Google.

Мне нужны действия, пацанчик!

Отлично, мне тоже! :) Давайте попробуем как установить Hydra и получить токен для клиентского приложения, его еще называют OAuth2 Client Grant (секция "Application Access"). Если вы не знаете как работать в консоли или у вас возникли вопросы, можете проконсультироваться на GitHub Issue Tracker.

Обратите внимание: в будущем, Hydra будет работать в Docker контейнере. Но сейчас, чтобы ее запустить, вам необходим Vagrant, VirtualBox и Git.

$ git clone https://github.com/ory-am/hydra.git
$ cd hydra
$ vagrant up

В рамках этого туториала я рассчитываю, что у вас не меняется рабочая директория. Я так же рассчитываю что вы работаете на Linux/Unix системе и у вас есть доступ к командной строке. Вы можете запускать vagrant ssh чтобы попасть в виртуальную машину. После этого вы можете выполнять все команды(например curl) без проблем. Только убедитесь что внутри виртуальной машине и избегайте всяких exit вызовов.

Для начала научимся запускать Hydra. Vagrant роутит 9000(HTTPS для Hydra) и 9001(для Postgres) на ваш локальный хост. Перейдите по URL https://localhost:9000/alive чтобы убедится, что Hydra запушен и все работает. Возможно, вам придется добавить исключение для HTTP сертификата, так-как он самоподписанный, и после это вы должны увидеть {"status":"alive"}, что означает что сервис Hydra запушен и работает.

Мы можем настроить тестовое клиентское приложение(id = app, secret = secret) с правами суперпользователя. Для этого в Vagrant нужно запустить команды:

$ hydra-host client create -i app -s secret -r http://localhost:3000/
authenticate/callback --as-superuser.

Инструмент hydra-host предоставляет набор возможностей для управления вашим инстансом Hydra. Вся информация по использованию есть в документации. Внутри Vagrant вы всегда можете спокойно добраться до hydra-host.

$ vagrant ssh
$ hydra-host help

Иногда, при загрузке у Vagrant возникают проблемы с сетью. Если вы не наблюдаете никаких других ошибок, например 404 в браузере, попробуйте выполнить vagrant destroy -f && vagrant up. После этого, в течении нескольких минут, Hydra поднимется и все должно заработать.

Получаем OAuth2 Token Client

Наконец то мы запустили Hydra, давайте теперь будем делать немного токен-магии. Я рассчитываю, что у вас установлен curl. Если это не так, то посмотрите как его установить. Наша цель заключается в "обмене"" учетных данных клиента на токен доступа.

Мы запускаем curl с параметром --insecure, так как у нас самоподписанный TLS сертификат. В вашем случае токен будет отличаться, но сам ответ сервера будет примерно таким же.

curl --insecure -X POST --user app:secret "https://localhost:9000/oauth2/
token?grant_type=client_credentials"

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIiLCJleHAiO
    jE0NTAzNTc4MDksImlhdCI6MTQ1MDM1NDIwOSwiaXNzIjoiIiwiamlkIjoiMmYyYjk2MGQtZjE3
    OC00MzE5LWI4OGEtODc3YzM1Y2U5NTFkIiwibmJmIjoxNDUwMzU0MjA5LCJzdWIiOiJhcHAifQ.
    cLSY3G0Ngz62hJmanADZ3LUfblB5nOZOWr7bAflE9T0pZBp-
    Qv1sTkwRCQfqv870cpHdFvN9xL_AReMmNo_o9sLmXfNZDL5WJzDhhsLximxPMD-
    rO0DjnvY5663l0fvhFMlaGREsHGWDzPN-wZLczRjlFr1JXPv80qMeCm9d343hGMu26WWZ8bfdgA
    bae8ecmSO_oP7I8U0tWn22FzVJjSRuaShKxlWyQY2K_0-VoHDQDZMTEIXxYGNPA0MmCOEK1DDAi
    UeKTbguMSLMCjXTkbxd2rMwHday1oHDH8aBkyL0CGmmfVfl20hfRYqJ0x7_0sTd__-
    inASEjozSvYkVOw",
    "expires_in": 3600,
    "token_type": "Bearer"
}

На текущий момент мы все еще обсуждаем, какие токены нужно использовать в Hydra. Работать только с самодостаточными JWT токенами или поддерживать различные типы? Если у вас есть идеи на этот счет, обязательно отпишитесь в дискуссии.

Получаем OAuth2 Token Password

Это было быстро, верно? Давайте теперь добавим обычную учетную запись для пользователя. Вы можете использовать что угодно в качестве имени пользователя(имя, email, случайный идентификатор), но оно должно быть уникальным. И обязательно запомните полученный ID, он пригодится нам в будущем.

$ vagrant ssh
$ hydra-host account create foo@bar.com --password secret

Created account as "e152f029-424f-4d4d-9d69-643225113ee5".

$ exit

Аутентифицируем пользователя с использованием паролей в OAuth2 .

curl --insecure --data "grant_type=password&username=foo@bar.
com&password=secret" --user app:secret "https://localhost:9000/oauth2/token"

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIiLCJleHAiOjE
  0NTAzNzAwODAsImlhdCI6MTQ1MDM2NjQ4MCwiaXNzIjoiIiwiamlkIjoiYTdjZDFmYWQtZTg5MS00
  ZDJmLWIwZmEtMzE2Zjg3MTI5ZGIyIiwibmJmIjoxNDUwMzY2NDgwLCJzdWIiOiJiY2U5M2QzMy05Y
  jVhLTQ5MzMtOTQ3Mi1jYWRhMDE4ZGFmNjAifQ.
  dqUHiAJ0uoUYtV4hqhgVqYqA6PSy1cmNZQruyTpmRaCBh2RHzkijFj4F-
  T8xTbrFBnysTQG3LxxeXkDNq6PZBsZ4WzvUXSy1R18MayT5FWkgAi-ROQ2lHn9Isw1IgN3XWO-
  YOaQt9rO0gG4w_hRQ-DprMMKcUkNVC1zK_pdUpaB7cEurYF3sd7krPQjIhucPVhJqDjkAIZGG54kd
  28_uLqKi3eTaDrViwGLbYzmLenfTb79Hxjfd8qFd_KBQW-f1maLy0BwQNP1pVu2I_P7CBjIwEm898
  wTPye42CFUfVzyvB6ob4sAZM60YVwzxN_zaw_SO1160HbDI4oO-HwwPig",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
  eyJpZCI6IjBlNmJmNTBlLTU1N2EtNGJiYy1iZDk1LTg3ZDJkOTNkOGQ5YSJ9.JFVgu7Tf1BZJLrMb
  gKi0wyBKXZuHB63yKbv6_UP8TUkUgH8e9S5Gi9MhlPOnU0KyiEkh8p5Z0CMN2HQeIeYj-
  0p3POFxoSkY6NPZeWKsnPXzDjlJJmXWYrqgI-N-
  BD26MmoGXLjHt_DY3hxBX_EzHHuqVk9q-2pUAfwc0BHjSidF5EZ852I5e3J0WHbiw4KnogNRKNN-l
  siIIEBSjkBxyyH85Dx4JdQZsAJVBKiXXzizWIQeQABAIutvIs5ok3T4xD8WYEiSuiHdKbPKe9bjNG
  X2OqW1X-eDts4RE0eHWatNQ-IafwMvi-7A0f5PSf26pSGPQ5TyvpA5qbnYAIXrMw",
  "token_type": "Bearer"
}

Процесс авторизации в OAuth2

Теперь посмотрим как работает процесс авторизации в OAuth2. Для этого на нужен ID аккаунта из примера выше. Так как Hydra это только бекенд часть, вам нужно будет использовать пример приложения для логина/разлогирования. Вам стоит посмотреть код в приложениях hydra-signin и hydra-signup.

$ vagrant ssh
$ ACCOUNT_ID=<account_id_from_above> hydra-signin & exit

Вам нужно заменить <...> своими данными, например:

$ vagrant ssh
$ ACCOUNT_ID=e152f029-424f-4d4d-9d69-643225113ee5 hydra-signin & exit

Теперь в браузере перейдите на по ссылке: https://localhost:9000/oauth2/auth?response_type=code&client_id=app&redirect_uri=http://localhost:3000/authenticate/callback&state=foo.

Sign in page

Кликните по ссылке "Press this link to sign in" для перехода на следующую страницу.

Sign in callback page

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

Для следующего шага мы будем использовать curl и полученный ранее код.

curl --insecure --data "grant_type=authorization_code&code=<code_crom_above>" -
-user app:secret "https://localhost:9000/oauth2/token"

Замените <...> вашими значениями, например:

curl --insecure --data 
"grant_type=authorization_code&code=fEat4PS3TVeyWrwKgLxICg" --user app:secret 
"https://localhost:9000/oauth2/token"

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIiLCJleHAiOjE
  0NTAzNTkwNDMsImlhdCI6MTQ1MDM1NTQ0MywiaXNzIjoiIiwiamlkIjoiZmMzODg4NjktZWE2MC00
  YzE4LWI1NmMtM2I4YmYzOTJmMzU5IiwibmJmIjoxNDUwMzU1NDQzLCJzdWIiOiJhcHAifQ.foEvIJ
  X3hwuCJCQvIi6x31m3g1VQ0RAp6ouiiVFIs2mVM7GsD2O3aS8WxlKaxZ5P7VhbJpxTR2zg9GDSGRe
  -Acj26r1OVjY9QSoLIeMNg2VfA6AwpASmYhP8EOdlbyjFEK8hC14JXToWn-
  cT6UXE0IZxg0ANevzDSHlPnaLDemNBkxoQ1cQPIOxPOz7xZSSDZmw9rv-MNlPi6F-
  FNZOEig5iEyl5vzDgExr5438Qkmc5OzlLYz-
  RoOroFtiyoqPXp0aYEms4zaowzB4m_DrQd0cIuAKjrtlUnbvId0rOnx-
  PBtF6yWZfSC7_hmWwtfrmho-XFWfaawjZswRWTAgaMg",
  "expires_in": 3600,
  "token_type": "Bearer"
}

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

Политики

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

{
    // Это должен быть уникальный ID. По этому ID
    // в базе данных будет производится поиск.
    id: "68819e5a-738b-41ec-b03c-b58a1b19d043",

    // Описание для человеков. Это не обязательное поле.
    description: "описание для людей",

    // К кому применять эту политику?
    // Обратите внимание, что тут можно использовать 
    // регулярные выражение внутри < >.
    subjects: ["max", "peter", "<zac|ken>"],

    // Эта политика разрешает или запрещает?
    effect: "allow",

    // На какой ресурс действует эта политика?
    // И тут тоже можно использовать регулярные 
    // выражения внутри < >.
    resources: ["urn:something:resource_a", "urn:something:resource_b", "urn:something:foo:<.+>"],

    // На какие права доступа влияет эта политика.
    // И тут тоже используем регулярные выражения.
    permissions: ["<create|delete>", "get"],

    // При каких условия политики начинают работать.
    conditions: [
        // В этом примере только при условия срабатывания "SubjectIsOwner
        {
            "op": "SubjectIsOwner"
        }
    ]
}

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

  • subject - это может быт учетная запись или клиентское приложение.
  • resource - некоторая онлайн страничка или файл в облаке.
  • permission - также может быть любой действие, например "создать", "удалить" и все в таком же роде.
  • condition - это некоторое логическое условие(например, является ли владельцем пользователь, запрашивающий ресурс?). На данный момент доступна только условие SubjectIsOwner. В скором будущем будет добавлено множество условий, таких как IPAddressMatches или UserAgentMatches.
  • effect может быть только allow или deny.

Как вы помните, тестовое клиентское приложение (app) добавлено с правами супер-пользователя, а ваш тестовый пользователь без прав супер-пользователя. Давайте посмотрим, что это значит в реальной жизни.

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

curl --insecure --data "grant_type=client_credentials&username=foo@bar.
com&password=secret" --user app:secret "https://localhost:9000/oauth2/token"

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIiLCJleHAiOjE
  0NTAzNTkwNDMsImlhdCI6MTQ1MDM1NTQ0MywiaXNzIjoiIiwiamlkIjoiZmMzODg4NjktZWE2MC00
  YzE4LWI1NmMtM2I4YmYzOTJmMzU5IiwibmJmIjoxNDUwMzU1NDQzLCJzdWIiOiJhcHAifQ.foEvIJ
  X3hwuCJCQvIi6x31m3g1VQ0RAp6ouiiVFIs2mVM7GsD2O3aS8WxlKaxZ5P7VhbJpxTR2zg9GDSGRe
  -Acj26r1OVjY9QSoLIeMNg2VfA6AwpASmYhP8EOdlbyjFEK8hC14JXToWn-
  cT6UXE0IZxg0ANevzDSHlPnaLDemNBkxoQ1cQPIOxPOz7xZSSDZmw9rv-MNlPi6F-
  FNZOEig5iEyl5vzDgExr5438Qkmc5OzlLYz-
  RoOroFtiyoqPXp0aYEms4zaowzB4m_DrQd0cIuAKjrtlUnbvId0rOnx-
  PBtF6yWZfSC7_hmWwtfrmho-XFWfaawjZswRWTAgaMg",
  "expires_in": 3600,
  "token_type": "Bearer"
}

В Hydra нужно передать определенную информацию для получения доступа:

  • Ресурс: к какому ресурсу нужен доступ?
  • Доступы: какие доступы были запрошены?
  • Токен: с каким токеном пришел запрос?
  • Контекст: например, ID пользователя.
  • Заголовок Authorization: Bearer <token> с валидным токеном, таким образом, анонимным пользователям тут не место.

Для проверки, имеет ли клиент права на создание ресурса "filA.png", нужно воспользоваться curl запросом с указанием токена, полученного выше, в теле POST (–data "...token=...") с использованием учетных данных клиента (–user app:secret):

curl --insecure \
--data '{"resource": "filA.png", "permission": "create", "token": 
"<client_token_from_above>"}' \
--user app:secret \
"https://localhost:9000/guard/allowed"

Замените <...> на ваши значения. Должно получится примерно так:

curl --insecure \
--data '{"resource": "filA.png", "permission": "create", "token": 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIiLCJleHAiOjE0NTAzNjkzOTQsImlhd
CI6MTQ1MDM2NTc5NCwiaXNzIjoiIiwiamlkIjoiZWZkM2M2ODMtZTQ3Ny00ODQ4LThmZTYtZWU4NGI1
YzAzZTUxIiwibmJmIjoxNDUwMzY1Nzk0LCJzdWIiOiJhcHAifQ.Q4zaiLaQvbVr9Ex3Oe9Htk-zhNsY
2mtxXQgtzvnxbIbWcvF2TE_fKoVAgOGQiUiF263CNVCpKqQkMGtWcm_c1fa_2r4HYXZvOoccxHrz7fo
aSuLDfqcfKinlhLn_UvERT5jR9sYOA5Vw7ES1cq2WdrP17LXog9V40I0aZzmhqHXFdAv5vb4y5MdUKp
aJgR_PWLBE_c12nmCRrLceSgHzVAVEyxW0BkUAK4cypIH0cz-
lsSPsFZLUogQQi0oBON3FVEuXeNBxJb-Ecp3V3C5aKjrg2bs0OKeJt-
ZItrzfsQF4Gsgh2irpLfF4tMN6fNDosulNT5-HuGLJGfzJzT2RYQ"}' \
--user app:secret \
"https://localhost:9000/guard/allowed"

{"allowed": true}

Теперь мы можем проверить, имеет ли тестовый пользователь foo@bar.com возможность создать ресурс "fileA.png". Спойлер: нет, не имеет, так как он не суперпользователь и мы не добавляли никаких дополнительных настроек. Для начала, получите токен для клиента, потом токен для пользователя:

curl --insecure --data "grant_type=password&username=foo@bar.
com&password=secret" --user app:secret "https://localhost:9000/oauth2/token"

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIiLCJleHAiOjE
  0NTAzNzAwODAsImlhdCI6MTQ1MDM2NjQ4MCwiaXNzIjoiIiwiamlkIjoiYTdjZDFmYWQtZTg5MS00
  ZDJmLWIwZmEtMzE2Zjg3MTI5ZGIyIiwibmJmIjoxNDUwMzY2NDgwLCJzdWIiOiJiY2U5M2QzMy05Y
  jVhLTQ5MzMtOTQ3Mi1jYWRhMDE4ZGFmNjAifQ.
  dqUHiAJ0uoUYtV4hqhgVqYqA6PSy1cmNZQruyTpmRaCBh2RHzkijFj4F-
  T8xTbrFBnysTQG3LxxeXkDNq6PZBsZ4WzvUXSy1R18MayT5FWkgAi-ROQ2lHn9Isw1IgN3XWO-
  YOaQt9rO0gG4w_hRQ-DprMMKcUkNVC1zK_pdUpaB7cEurYF3sd7krPQjIhucPVhJqDjkAIZGG54kd
  28_uLqKi3eTaDrViwGLbYzmLenfTb79Hxjfd8qFd_KBQW-f1maLy0BwQNP1pVu2I_P7CBjIwEm898
  wTPye42CFUfVzyvB6ob4sAZM60YVwzxN_zaw_SO1160HbDI4oO-HwwPig",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
  eyJpZCI6IjBlNmJmNTBlLTU1N2EtNGJiYy1iZDk1LTg3ZDJkOTNkOGQ5YSJ9.JFVgu7Tf1BZJLrMb
  gKi0wyBKXZuHB63yKbv6_UP8TUkUgH8e9S5Gi9MhlPOnU0KyiEkh8p5Z0CMN2HQeIeYj-
  0p3POFxoSkY6NPZeWKsnPXzDjlJJmXWYrqgI-N-
  BD26MmoGXLjHt_DY3hxBX_EzHHuqVk9q-2pUAfwc0BHjSidF5EZ852I5e3J0WHbiw4KnogNRKNN-l
  siIIEBSjkBxyyH85Dx4JdQZsAJVBKiXXzizWIQeQABAIutvIs5ok3T4xD8WYEiSuiHdKbPKe9bjNG
  X2OqW1X-eDts4RE0eHWatNQ-IafwMvi-7A0f5PSf26pSGPQ5TyvpA5qbnYAIXrMw",
  "token_type": "Bearer"
}

Команда, которую вам нужно выполнить, очень простая, но в этот раз нам нужно указать токен пользователя внутри JSON в теле запроса.

curl --insecure \
--data '{"resource": "filA.png", "permission": "create", "token": 
"<ACCOUNT_token_from_above>"}' \
--user app:secret \
"https://localhost:9000/guard/allowed"
curl --insecure \
--data '{"resource": "filA.png", "permission": "create", "token": 
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIiLCJleHAiOjE0NTAzNzAwODAsImlhd
CI6MTQ1MDM2NjQ4MCwiaXNzIjoiIiwiamlkIjoiYTdjZDFmYWQtZTg5MS00ZDJmLWIwZmEtMzE2Zjg3
MTI5ZGIyIiwibmJmIjoxNDUwMzY2NDgwLCJzdWIiOiJiY2U5M2QzMy05YjVhLTQ5MzMtOTQ3Mi1jYWR
hMDE4ZGFmNjAifQ.dqUHiAJ0uoUYtV4hqhgVqYqA6PSy1cmNZQruyTpmRaCBh2RHzkijFj4F-
T8xTbrFBnysTQG3LxxeXkDNq6PZBsZ4WzvUXSy1R18MayT5FWkgAi-ROQ2lHn9Isw1IgN3XWO-
YOaQt9rO0gG4w_hRQ-DprMMKcUkNVC1zK_pdUpaB7cEurYF3sd7krPQjIhucPVhJqDjkAIZGG54kd28
_uLqKi3eTaDrViwGLbYzmLenfTb79Hxjfd8qFd_KBQW-f1maLy0BwQNP1pVu2I_P7CBjIwEm898wTPy
e42CFUfVzyvB6ob4sAZM60YVwzxN_zaw_SO1160HbDI4oO-HwwPig"}' \
--user app:secret \
"https://localhost:9000/guard/allowed"
{"allowed": false}

Ух! Тут было много копипасты команд в консоль, но у вас получилось. Вы попробовали основные возможности Hydra. Конечно, мы прошлись поверхностно только по базовым возможностям Hydra и есть еще много чего для более подробного исследования. Команда Ори надеется, что вам понравился этот туториал и вы будете рекомендовать Hydra друзьям. Пока что Hydra не так стабильна, как хотелось бы, но мы работаем над этим. Если вы нашли баг, обязательно сообщите нам об этом.

За лого я благодарен pathfinderlinden.