Django - это популярный веб-фреймворк на основе Python. Это огромный так называемый “battery-included” фреймворк, охватывающий многие аспекты веб-разработки: аутентификацию, ORM, формы, панели администратора и т.д. Это также фреймворк с сильным мнением, который предлагает шаблоны почти для всего, что вы делаете, что позволяет вам чувствовать себя хорошо управляемым во время разработки.
Однако в последние несколько лет Django, как и большинство не-JS стеков, уступает свои позиции JS-фреймворкам, таким как Next.js, Remix, Nuxt и др.
Переход от одного фреймворка к другому требует тщательного планирования и исполнения, особенно если вы одновременно меняете язык. Популярным и мощным Javascript/Typescript эквивалентным стеком для Django может быть следующая комбинация:
- Next.js: маршрутизация URL, SSR и создание страниц с помощью ReactJS (слой представления + шаблонов Django).
- NextAuth: аутентификация (аутентификация Django)
- Prisma: ORM + миграция баз данных (слой моделей Django).
Эти части очень хорошо сочетаются друг с другом и достаточны для замены большинства возможностей, которые предоставляет Django. Однако одного элемента не хватает. В Django есть встроенная функция разрешений, но она ограничена контролем на уровне модели, т.е. если пользователь или группа имеют доступ X к модели типа Y. Многие пользователи используют популярный пакет django-guardian для реализации разрешений на уровне строк. Он позволяет устанавливать разрешения между пользователями/группами и объектами, управляет базовыми таблицами базы данных разрешений и предоставляет API для настройки и проверки таких разрешений.
К счастью, если вы решите использовать Prisma ORM в своем новом стеке, вы можете использовать ZenStack для достижения аналогичных функций с меньшими усилиями. ZenStack - это набор инструментов, созданный как расширение возможностей Prisma ORM, и одним из его основных направлений является контроль доступа. В этом посте мы кратко сравним, как django-guardian и ZenStack решают проблему разрешений на уровне строк, соответственно.
Оглавление
Назначение разрешений
Предположим, мы создаем сайт для ведения блогов и имеем модель Post. В Django уже есть встроенные модели User и Group и предопределенные CRUD разрешения для каждой модели. С помощью django-guardian вы можете использовать API assign_perm для назначения разрешений:
from django.contrib.auth.models import User, Group
from guardian.shortcuts import assign_perm
# establishing permission between a user and a post
user1 = User.objects.create(username='user1')
post1 = Post.object.create(title='My Post', slug='post1')
assign_perm('view_post', user1, post1)
assign_perm('change_post', user1, post1)
# establishing permission between a group and a post
group1 = Group.objects.create(name='group1')
user1.groups.add(group1)
assign_perm('view_post', group1, post1)
assign_perm('change_post', group1, post1)
В отличие от Django, Next.js + Prisma + ZenStack является неориентированным фреймворком и не имеет встроенных моделей для User и Group. Вам необходимо явно смоделировать их с помощью схемы ZenStack:
model User {
id Int @id @default(autoincrement())
username String
groups Group[]
}
model Group {
id Int @id @default(autoincrement())
name String
users User[]
}
Схема не только моделирует типы данных и отношения, но и позволяет выразить в ней полномочия. Давайте посмотрим, как смоделировать разрешения пользователя и группы на пост:
model User {
id Int @id @default(autoincrement())
username String
groups Group[]
posts Post[]
}
model Group {
id Int @id @default(autoincrement())
name String
users User[]
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
slug String @unique
groups Group[]
users User[]
// if the current user is in the user list of the post, update is allowed
@@allow('read,update', users?[id == auth().id])
// if the current user is in any group of the group list of the post,
// update is allowed
@@allow('read,update', groups?[users?[id == auth().id]])
// ... other permissions
}
Некоторые разъяснения:
- Синтаксис @@allow добавляет метаданные контроля доступа к модели. Действие разрешено, если любое из правил @@allow имеет значение true.
- auth() представляет текущего пользователя для входа в систему. Вскоре вы увидите, как он подключается.
- Синтаксис model?[expression] представляет собой предикат над отношением to-many. users?[id == auth().id] читается как "имеет ли любой элемент в коллекции users id, равный id текущего пользователя".
Как вы можете видеть, подход к моделированию разрешений совершенно разный для django-guardian и ZenStack. Django-guardian использует императивный код для управления разрешениями в коде приложения, в то время как ZenStack предпочитает декларативный стиль в схеме данных. Кроме того, в django-guardian установка и проверка разрешений (показанные в следующем разделе) разделены, в то время как в ZenStack вы моделируете данные о разрешениях и правила в одном месте.
Проверка разрешений
Как и в случае с назначением разрешений, в django-guardian проверка разрешений также выполняется явно в коде приложения, в основном одним из двух способов:
1. Используя императивный код
user1 = User.objects.get(username='user1')
post1 = Post.objects.get(slug='post1')
from guardian.core import ObjectPermissionChecker
checker = ObjectPermissionChecker(user1)
if checker.has_perm('change_post', post1):
# update logic here
2. Использование декораторов
Вы также можете использовать декораторы для включения автоматической проверки разрешений в представлениях:
from guardian.decorators import permission_required_or_403
@permission_required_or_403('change_post', (Post, 'slug', 'post_slug'))
def edit_post(request, post_slug):
# update logic here
Независимо от того, какой метод вы используете, вы должны обеспечить добавление проверки везде, где необходимо проверить разрешения.
В ZenStack проверка разрешений намного проще, поскольку правила выражены на уровне ORM, поэтому они автоматически применяются при вызове уровня данных:
Когда перечисляются посты, возвращаются только те, которые принадлежат текущему пользователю или его группе.
Когда пост обновляется, операция отклоняется, если пользователь не принадлежит к посту или какой-либо группе этого поста.
Единственная необходимая настройка - создать клиентскую обертку Prisma с контролем доступа и текущим пользователем в качестве контекста:
// update-post.ts: function for updating a post
import { prisma } from './db';
import { getSessionUser } from './auth';
export function updatePost(request: Request, slug: string, data: PostUpdateInput) {
const user = await getSessionUser(req);
// get an access-control enabled Prisma wrapper
// the "user" context value supports the `auth()`
// function in the permission rules
const db = withPresets(prisma, { user });
// error will be thrown if the current user doesn't
// have permission
return db.post.update({ where: { slug }, data });
}
Подведение итогов
Как видите, и django-guardian, и ZenStack решают проблемы с разрешениями на уровне строк, хотя и в совершенно разных парадигмах. Django-guardian полагается на императивный код, и ответственность за то, чтобы обеспечить надлежащую логику проверки там, где это необходимо, в большей степени лежит на разработчике.
С другой стороны, ZenStack отдает предпочтение декларативному моделированию. Поскольку правила выражаются на уровне ORM, они автоматически применяются ко всему коду приложения, использующему уровень данных, что повышает согласованность и надежность.
Будучи относительно новым инструментарием, ZenStack не лишен своих ограничений. Например, по сравнению с django-guardian, в нем отсутствуют две основные функции:
Пользовательские разрешения
ZenStack моделирует фиксированный набор разрешений: CRUD, в то время как django-guardian позволяет вам определять пользовательские разрешения. Хотя все разрешения в конечном итоге сводятся к CRUD, пользовательские разрешения могут выражать тонкий контроль разрешений на уровне полей. Это пока не поддерживается ZenStack.
API для явной проверки разрешений
Проверка разрешений в ZenStack внедрена в CRUD API ORM. Однако иногда бывает удобно явно проверить, есть ли у пользователя разрешение на определенный объект, и использовать его, например, для динамического отображения пользовательского интерфейса.
Я надеюсь, что эта статья поможет вам на пути перехода вашего стека python в мир Javascript, и удачи вам в этом начинании!