Правильная модель авторизации для вашего приложения

Перевод статьи «What’s the Right Authorization Model for My Application?».

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

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

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

Вы начинаете работать над MVP этого проекта и хотите итерироваться по мере развития продукта. Проект вы создаете на Ruby On Rails, потому что хорошо в нем разбираетесь.

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

Первый шаг: аутентификация

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

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

Часто люди путают термины «аутентификация» и «авторизация». Основная причина заключается в том, что аутентификация обычно является первым шагом в авторизации.

Аутентификация — это процесс проверки того, что человек является тем, кем представляется, а авторизация — процесс проверки того, к чему он может получить доступ, а к чему нет.

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

Как же сделать так, чтобы только некоторые пользователи могли выполнять операции с отчетами?

Использование ролей

Ваши пользователи могут отправлять отчеты, но на данном этапе вам может понадобиться группа пользователей, отвечающая за их утверждение. Для этого нужно определить роль «Утвердитель расходов» (англ. expense approver)

Если при выборе подходящей модели авторизации вы понимаете, что можете сгруппировать пользователей на основе их общих обязанностей, вам, вероятно, подойдет управление доступом на основе ролей (англ. Role-Based Access Control, RBAC).

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

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

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

Использование разрешений

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

Разрешение (англ. permission) — это объявление действия, которое может быть выполнено над ресурсом. Разрешения, определенные для API в Auth0, могут быть использованы как часть механизма контроля доступа, используемого вашим приложением.

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

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

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

Чтобы остальные пользователи могли видеть только свои отчеты, им тоже требуется разрешение «Читать отчет о расходах». Но поскольку у них нет роли «Утвердитель расходов», логика приложения может запретить им читать любые отчеты о расходах, кроме своих собственных. Использование ролей в сочетании с разрешениями позволяет вашему приложению определять, какие отчеты пользователь может читать, а какие нет.

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

def can_read_expense_report(user)

  return true if user.expense_approver?

  expenses = db.fetch(
    "SELECT expenses.id, users.user_id
     FROM expenses WHERE id = %
     JOIN users WHERE users.user_id = expenses.submitter_id")

  authorize!(user, :read, :expenses)
end

Другой вариант — использовать внешний гем, например CanCanCan или pundit. Детали реализации полностью зависят от вас и вашего приложения.

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

Вы делаете глубокий вдох и начинаете обдумывать ситуацию…

Использование атрибутов для управления доступом

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

Когда вы определяете систему авторизации, которая оценивает атрибуты (или характеристики), вы используете управление доступом на основе атрибутов (Attribute-Based Access Control, ABAC).

Цель ABAC — защитить объекты от неавторизованных пользователей и действий. Т.е. от пользователей и действий, которые не имеют «одобренных» характеристик, определенных политикой безопасности организации.

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

Если пользователь A является менеджером пользователя B, то пользователь A может одобрять отчеты о расходах от пользователя B.

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

def approve_expense(user)
  expense = db.fetch(
    "SELECT expenses.*, users.manager_id as submitter_manager_id
     FROM expenses WHERE id = %
     JOIN users WHERE users.user_id = expenses.submitter_id")

  authorize!(user, :approve, :expense)
end

Ключевое отличие заключается в том, что вы основываете метод approve_expense и правила на атрибутах объекта, в данном случае объекта user. Как видите, ABAC, RBAC и разрешения можно использовать вместе.

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

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

Кроме того, если в политике авторизации произойдут какие-либо изменения, это потребует от вас внесения изменений в код приложения.

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

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

Мне нужно предоставлять доступ на основе отношений

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

Управление доступом на основе отношений (англ. Relationship-Based Access Control, ReBAC) позволяет вам выражать правила на основе отношений, которые пользователи имеют с объектами, а объекты — с другими объектами. Это обеспечивает более выразительную модель авторизации, которая может описывать очень сложные контексты.

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

ReBAC — это пример модели, которая, как правило, обеспечивает наибольшую гибкость. Модели с высокой степенью гибкости часто называют термином Fine-Graned Authorization (FGA).

Прим. перев.: этот термин можно перевести как «тонкая авторизация»; fine-grain также переводится как «мелкого уровня (разбиения); на уровне мелких структурных единиц».

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

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

Кажется, что модель тонкой авторизации — именно то, что нужно для вашего случая🤩. Но подождите. Наверняка что-то подобное будет сложно создать, верно? Да, это так. Однако хорошая новость заключается в том, что мы уже сделали это за вас! Auth0 FGA предоставляет простое решение и предлагает способ определить вашу модель авторизации и отношения между пользователем и отчетом, используя что-то вроде следующего:

type report
  relations
    define approver: can_manage from submitter
    define submitter: [user]
type user
  relations
    define can_manage: manager or can_manage from manager
    define manager: [user]

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

Если вы хотите проверить, является ли пользователь менеджером другого пользователя в иерархии управления, вы можете использовать API Auth0 FGA Check. На языке Ruby это будет выглядеть следующим образом:

require "uri"
require "json"
require "net/http"

FGA_API_URL="YOUR_FGA_API_URL"
FGA_BEARER_TOKEN="YOUR_BEARER_TOKEN"
FGA_STORE_ID="YOUR_STORE_ID"

url = URI("#{FGA_API_URL}/stores/#{FGA_STORE_ID}/check")

http = Net::HTTP.new(url.host, url.port);
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Authorization"] = "Bearer #{FGA_BEARER_TOKEN}"
request["content-type"] = "application/json"
request.body = JSON.dump({
  "tuple_key": {
    "user": "user:alex",
    "relation": "can_manage",
    "object": "user:sam"
  }
})

response = http.request(request)
puts response.read_body

# Response: {"allowed":true,"resolution":""}

Преимуществом использования сторонних сервисов для управления системой авторизации является то, что вы удаляете логику авторизации из кода приложения и обеспечиваете хранение всех политик в одном месте, что упрощает их поддержку. Вы также минимизируете задержки в запросах, поскольку Auth0 FGA оптимизирован для обеспечения надежности и низких задержек в больших масштабах. Что касается вашего конкретного случая, вы сократите задержки еще больше, поскольку не будете обращаться к своей базе данных каждый раз, когда захотите проверить, может ли пользователь одобрить отчет о расходах.

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

Заключение

Аутентификация в системе нужна почти всегда, но обычно недостаточно аутентифицировать пользователей, чтобы проверить, есть ли у них доступ к ресурсу. Модель управления доступом на основе ролей (RBAC) может быть достаточной для простых приложений, особенно если у вас несколько ролей. Однако, если вам нужна более детальная модель управления доступом, вы можете использовать разрешения и назначать их пользователям независимо от их ролей. Вы можете определить контроль доступа на основе некоторых атрибутов пользователя, например, реализовав ABAC, или ограничить доступ к ресурсам на основе объектных отношений с помощью ReBAC.

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

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх