javascript

CASL. Авторизация для JavaScript приложения

  • пятница, 28 июля 2017 г. в 03:12:02
https://habrahabr.ru/post/334076/
  • Node.JS
  • JavaScript


imageВ наше время почти каждое приложение имеет понятие прав доступа и предоставляет различные функции для разных групп пользователей (например, admin, member, subscriber и т.д.). Эти группы обычно называются "роли".


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


CASL — это библиотека для авторизации в JavaScript, которая заставляет задумываться о том, что пользователь может делать в системе, а не какую роль он имеет (проверка звучит так: если пользователь имеет эту способность, то он может сделай это). Например, в приложении для блогинга пользователь может создавать, редактировать, удалять, просматривать статьи и комментарии. Давайте разделим эти способности между двумя группами пользователей: анонимными пользователями (теми, кто не идентифицировался в системе) и писателями (теми, кто идентифицировался в системе).


Анонимные пользователи могут только читать статьи и комментарии. Писатели могут делать то же самое плюс управлять своими статьями и комментариями (в этом случае "управлять" означает создавать, читать, обновлять и удалять). При помощи CASL это можно записать вот так:


import { AbilityBuilder } from 'casl'

const user = whateverLogicToGetUser()
const ability = AbilityBuidler.define(can => {
  can('read', ['Post', 'Comment'])

  if (user.isLoggedIn) {
    can('create', 'Post')
    can('manage', ['Post', 'Comment'], { authorId: user.id })
  }
})

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


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


can('delete', 'Post', { 'comments.0': { $exists: false } })

Проверяем возможности


Существует 3 метода у экземпляра Ability, которые позволяют проверять права доступа:


import { ForbiddenError } from 'casl'

ability.can('update', 'Post')
ability.cannot('update', 'Post')

try {
  ability.throwUnlessCan('update', 'Post')
} catch (error) {
  console.log(error instanceof Error) // true
  console.log(error instanceof ForbiddenError) // true
}

Первый метод вернет false, второй true, а третий выбросит ForbiddenError для анонимного пользователя, так как они не имеют права обновлять статьи. В качестве второго аргумента, эти методы могут принимать экземпляр класса:


const post = new Post({ title: 'What is CASL?' })

ability.can('read', post)

В этом случае can ('read', post) возвращает true, потому что в способностях мы определили, что пользователь может читать все статьи. Тип объекта вычисляется на основе constructor.name. Его можно переопределить создав статическое свойство modelName на классе Post, это может понадобится если для продакшн сборки используется минификация имен функций. Также можно написать свою функцию по определению типа объекта и передать ее как опцию в конструктор Ability:


import { Ability } from 'casl'

function subjectName(subject) {
  // custom logic to detect subject name, should return string or undefined
}

const ability = new Ability([], { subjectName })

Давайте теперь проверим случай, когда пользователь пытается обновить статью другого пользователя (я буду ссылаться на идентификатор другого автора как anotherId и к идентификатору текущего пользователя как myId):


const post = new Post({ title: 'What is CASL?', authorId: 'anotherId' })

ability.can('update', post)

В этом случае can('update', post) возвращает false, поскольку мы определили, что пользователь может обновлять только свои собственные статьи. Конечно же, если проверить то же самое на собственной статье то получим true. Подробнее о проверках прав доступа можно посмотреть в разделе Check Abilities в официальной документации.


Интеграция с базой данных


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


Для конвертации прав доступа в Mongo запрос существует toMongoQuery функция:


import { toMongoQuery } from 'casl'

const query = toMongoQuery(ability.rulesFor('read', 'Post'))

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


// { $or: [{ authorId: 'myId' }] }
const query = toMongoQuery(ability.rulesFor('update', 'Post'))

Теперь query содержит запрос, который должен возвращать только те записи, которые были созданы мной. Все обычные правила проходят через цепочку логического OR, поэтому вы и видите $or оператор в результате запроса.


Также CASL предоставляет плагин под mongoose, который добавляет accessibleBy метод к моделям. Этот метод под капотом вызывает функцию toMongoQuery и передает результат в метод find mongoose-a.


Пример использования с mongoose
const { mongoosePlugin, AbilityBuilder } = require('casl')
const mongoose = require('mongoose')

mongoose.plugin(mongoosePlugin)

const Post = mongoose.model('Post', mongoose.Schema({
  title: String,
  author: String,
  content: String,
  createdAt: Date
}))

// by default it asks for `read` rules and returns mongoose Query, so you can chain it
Post.accessibleBy(ability).where({ createdAt: { $gt: Date.now() - 24 * 3600 } })

// also you can call it on existing query to enforce visibility.
// In this case it returns empty array because rules does not allow to read Posts of `someoneelse` author
Post.find({ author: 'someoneelse' }).accessibleBy(ability, 'update').exec()

По умолчанию accessibleBy создаст запрос на базе read прав доступа. Чтобы построить запрос для другого действия, просто передайте его вторым аргументом. Более детально можно посмотреть в разделе Database Integration.


И напоследок


CASL написан на чистом ES6, поэтому его можно использовать для авторизации как на API так и на UI стороне. Дополнительным плюсом является то, что UI может запросить все права доступа с API, и использовать их, чтобы показать или скрыть кнопки или целые секции на странице.