RSS Мои друзья Контакты

CASL и Cancan. Как расшарить права доступа между UI и API

CASL Vuex + Rails API

С ростом количества и типов устройств приложения были разделены на два отдельных компонента: UI (front-end, то, что пользователь видит и взаимодействует) и API (back-end, подразумевает бизнес-правила). Эти 2 части могут быть написаны на одном языке (например, JavaScript) или на разных (например, JavaScript на UI и Ruby on API). Такой подход позволяет описать бизнес-правила всего один раз и реализовывать отдельные пользовательские интерфейсы для каждого типа устройства (например, Web, iOS, Android).

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

Управление этими правами может стать кошмаром для приложений, которые разделены на UI и API. Более того, если есть две разные команды, которые реализуют эти части, они, скорее всего, придут к созданию кода, который будет обрабатывать разрешения для UI и API отдельно. Таким образом, изменения в логике прав доступа back-end-а потребуют изменений на UI и наоборот.

Это увеличивает время для разработки и увеличивает количество ошибок в приложении. Так как же с этим бороться?

Делитесь правами доступа :)

Когда это возможно, определите права доступа на стороне сервера и поделись ими с клиентской стороной. Этот подход работает очень хорошо, если вы используете Node.js, потому что вы можете использовать CASL с обеих сторон и легко передавать правила с использованием токена JWT например.

Также CASL прекрасно работает с Ruby gem-ом cancan (не поддерживаемый, сообщество создало форк под названием cancancan). На самом деле этот gen был для меня большим вдохновением для создания CASL :)

Впервые слышете о CASL? Рекомендую прочесть Что такое CASL?

Пример интеграции

CASL Vuex + Rails API

Я создал пример, который показывает, как интегрировать API на базе Rails 5 с пользовательским интерфейсом на базе Vue. Это простое приложение для блога, которое позволяет управлять статьями, входить в приложение и выходить из него.

Для тех, кто интересен в конечном результате, перейдите по ссылкам Vue app и Rails API

REST API

На самом деле на серверной стороне нет особо тонкостей, которые требуют объяснений. Это обычное Rails приложение, которое отдает JSON в ответ на запросы. Вся логика авторизации была выполнена с помощью вспомогательной функции load_and_authorize_resource (за деталями обращайтесь к Wiki Cancancan проекта).

Сессия управляется с помощью JWT токена. Этот токен содержит только поле user_id. Для входа в систему клиент должен отправить запрос POST в /api/session с электронной почтой и паролем (более детально описано в README проекта). В случае успешного ответа сервер возвращает JWT токен и список правил, совместимых с форматом CASL.

Это ответ для пользователя с ролью "member":

{
    "token": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyfQ.3MA5pz-JXuSs3YHdIEJcokTpharBLjUmfzXGp1dyYY8",
    "rules": [
        {
            "actions": ["read"],
            "subject": ["all"]
        },
        {
            "actions": ["manage"],
            "subject": ["Article"],
            "conditions": {
                "author_id": 2
            }
        },
        {
            "actions": ["read", "update"],
            "subject": ["User"],
            "conditions": {
                "id": 2
            }
        }
    ]
}

Этот ответ говорит, что пользователь может читать все, может управлять статьями, в которых author_id равно 2, и может читать и обновлять пользователя id которого равно 2. Возможно, Вы уже поняли, что число 2 является идентификатором залогиненого пользователя. Проще говоря, он может управлять собственными статьями, обновлять собственный профиль и читать все остальное.

Cancan хранит правила не так, как CASL, поэтому я добавил метод to_list в Ability, что возвращает массив правил, который может использовать CASL:

class Ability
  include CanCan::Ability
  
  # ....
  
  def to_list
    rules.map do |rule|
      object = { actions: rule.actions, subject: rule.subjects.map{ |s| s.is_a?(Symbol) ? s : s.name } }
      object[:conditions] = rule.conditions unless rule.conditions.blank?
      object[:inverted] = true unless rule.base_behavior
      object
    end
  end
end

Vue app

CASL поставляется вместе с дополнительным пакетом для Vue. Этот пакет добавляет метод $can во все компоненты Vue и позволяет легко проверять права доступа в шаблонах. Например:

<template>
  <div v-if="$can('create', 'Post')">
    <a @click="createPost">Add Post</a>
  </div>
</template>

Это приложение использует Vuex для управления локальным состоянием. Все запросы к REST API сделаны с помощью actions в Vuex.Store. Также есть несколько плагинов и модулей, которые Вы можете найти в папке src/store, но самым интересным для нас является src/store/ability.js.

Этот плагин заполняет экземпляр Ability правами доступа когда пользователь логинится в систему и удаляет его при выходе из системы.

import { Ability } from '@casl/ability'

export const ability = new Ability()

export const abilityPlugin = (store) => {
  ability.update(store.state.rules)

  return store.subscribe((mutation) => {
    switch (mutation.type) {
    case 'createSession':
      ability.update(mutation.payload.rules)
      break
    case 'destroySession':
      ability.update([{ actions: 'read', subject: 'all' }])
      break
    }
  })
}

В приведенном выше коде я создаю и экспортирую пустой экземпляр Ability (этот экземпляр будет использоваться позже). Затем я создаю плагин для Vuex, который подписывается на сохранение изменений. Когда коммитится мутация createSession (т. е. вход пользователя), Ability обновляется указанными правами доступа и на destroySession (т.е., выход пользователя), способность сбрасывается в режим только для чтения.

Позже этот плагин подключается к хранилищу через свойство plugins и что способность реэкспортируется и передается в Vue abilitiesPlugin.

Вот и все!

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

P.S.: оригинальная статья была опубликована на Medium.com

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

Как видно из примера, CASL достаточно несложно интегрировать с существующими решениями. Если что-то было не понятно - пишите в комментариях или посмотрите в:

Добавить комментарий

Комментариев: 0