В наше время почти каждое приложение имеет понятие прав доступа и предоставляет различные функции для разных групп пользователей (например, 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.
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, и использовать их, чтобы показать или скрыть кнопки или целые секции на странице.