10. RBAC: ролевая модель
Что такое RBAC?
Заголовок раздела «Что такое RBAC?»RBAC (Role-Based Access Control) — управление доступом на основе ролей. Пользователям назначаются роли, ролям — разрешения.
Пользователь → Роль → Разрешения → Ресурс Alice → Admin → all ops → Everything Bob → Editor → read/write → Articles Carol → Viewer → read only → ArticlesПреимущества:
- Простота управления: меняешь роль, а не каждое разрешение
- Понятность: “Иван — Менеджер” понятнее чем список из 50 прав
- Масштабируемость: легко добавлять новые роли
Простая реализация RBAC
Заголовок раздела «Простая реализация RBAC»Модель данных (Prisma)
Заголовок раздела «Модель данных (Prisma)»model User { id String @id @default(cuid()) email String @unique role Role @default(USER)}
enum Role { USER EDITOR MODERATOR ADMIN}Middleware проверки роли
Заголовок раздела «Middleware проверки роли»// Простая проверка ролиfunction hasRole(requiredRole) { const roleHierarchy = { USER: 1, EDITOR: 2, MODERATOR: 3, ADMIN: 4, };
return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Unauthorized' }); }
const userLevel = roleHierarchy[req.user.role] || 0; const requiredLevel = roleHierarchy[requiredRole] || 0;
if (userLevel < requiredLevel) { return res.status(403).json({ error: `Required role: ${requiredRole}` }); }
next(); };}
// Использованиеapp.delete('/api/posts/:id', authMiddleware, hasRole('MODERATOR'), async (req, res) => { await db.posts.delete(req.params.id); res.json({ success: true });});Продвинутый RBAC с Permissions
Заголовок раздела «Продвинутый RBAC с Permissions»Роли + явные разрешения:
Модель данных
Заголовок раздела «Модель данных»model Permission { id String @id @default(cuid()) name String @unique // "posts:create", "users:delete" roles RolePermission[]}
model Role { id String @id @default(cuid()) name String @unique // "admin", "editor", "viewer" permissions RolePermission[] users UserRole[]}
model RolePermission { role Role @relation(fields: [roleId], references: [id]) roleId String permission Permission @relation(fields: [permissionId], references: [id]) permissionId String @@id([roleId, permissionId])}
model UserRole { user User @relation(fields: [userId], references: [id]) userId String role Role @relation(fields: [roleId], references: [id]) roleId String @@id([userId, roleId])}Сид начальных данных
Заголовок раздела «Сид начальных данных»async function seedRBAC() { // Создаём permissions const permissions = await Promise.all([ db.permission.upsert({ where: { name: 'posts:read' }, create: { name: 'posts:read' }, update: {} }), db.permission.upsert({ where: { name: 'posts:create' }, create: { name: 'posts:create' }, update: {} }), db.permission.upsert({ where: { name: 'posts:update' }, create: { name: 'posts:update' }, update: {} }), db.permission.upsert({ where: { name: 'posts:delete' }, create: { name: 'posts:delete' }, update: {} }), db.permission.upsert({ where: { name: 'users:manage' }, create: { name: 'users:manage' }, update: {} }), ]);
// Создаём роли с правами const viewer = await db.role.upsert({ where: { name: 'viewer' }, create: { name: 'viewer', permissions: { create: [{ permissionId: permissions[0].id }] }, }, update: {}, });
const editor = await db.role.upsert({ where: { name: 'editor' }, create: { name: 'editor', permissions: { create: [0, 1, 2].map(i => ({ permissionId: permissions[i].id })), }, }, update: {}, });
const admin = await db.role.upsert({ where: { name: 'admin' }, create: { name: 'admin', permissions: { create: permissions.map(p => ({ permissionId: p.id })), }, }, update: {}, });
console.log('RBAC seeded!');}Проверка разрешений
Заголовок раздела «Проверка разрешений»// Загружаем разрешения пользователя при логинеasync function getUserPermissions(userId) { const user = await db.user.findUnique({ where: { id: userId }, include: { roles: { include: { role: { include: { permissions: { include: { permission: true }, }, }, }, }, }, }, });
// Собираем все разрешения из всех ролей const permissions = new Set( user.roles.flatMap(ur => ur.role.permissions.map(rp => rp.permission.name) ) );
return Array.from(permissions);}
// Middleware для проверки разрешенияfunction can(permission) { return async (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Unauthorized' }); }
// Можно кешировать в Redis const permissions = await getUserPermissions(req.user.id);
if (!permissions.includes(permission)) { return res.status(403).json({ error: `Permission required: ${permission}` }); }
next(); };}
// Использованиеapp.post('/api/posts', authMiddleware, can('posts:create'), createPost);app.put('/api/posts/:id', authMiddleware, can('posts:update'), updatePost);app.delete('/api/posts/:id', authMiddleware, can('posts:delete'), deletePost);app.get('/api/users', authMiddleware, can('users:manage'), getUsers);RBAC в Next.js
Заголовок раздела «RBAC в Next.js»Middleware защита роутов
Заголовок раздела «Middleware защита роутов»import { auth } from '@/auth';
const routePermissions: Record<string, string[]> = { '/admin': ['admin'], '/editor': ['admin', 'editor'], '/dashboard': ['admin', 'editor', 'viewer'],};
export default auth((req) => { const session = req.auth; const pathname = req.nextUrl.pathname;
// Находим требуемые роли для пути const requiredRoles = Object.entries(routePermissions) .find(([path]) => pathname.startsWith(path))?.[1];
if (!requiredRoles) return; // путь не защищён
if (!session?.user) { return Response.redirect(new URL('/login', req.url)); }
if (!requiredRoles.includes(session.user.role)) { return Response.redirect(new URL('/403', req.url)); }});Server Component guard
Заголовок раздела «Server Component guard»import { auth } from '@/auth';import { redirect } from 'next/navigation';
export async function requirePermission(permission: string) { const session = await auth();
if (!session?.user) redirect('/login');
const permissions = await getUserPermissions(session.user.id);
if (!permissions.includes(permission)) { redirect('/403'); }
return session;}
// app/admin/page.tsximport { requirePermission } from '@/lib/rbac';
export default async function AdminPage() { await requirePermission('users:manage');
return <div>Admin Dashboard</div>;}UI на основе разрешений
Заголовок раздела «UI на основе разрешений»'use client';
import { useSession } from 'next-auth/react';
function AdminButton() { const { data: session } = useSession();
if (session?.user?.role !== 'admin') return null;
return <button>Admin Action</button>;}
// Или универсальный компонентfunction Can({ permission, children }: { permission: string; children: React.ReactNode}) { const { data: session } = useSession();
if (!session?.user?.permissions?.includes(permission)) return null;
return <>{children}</>;}
// Использование<Can permission="posts:delete"> <DeleteButton /></Can>Практические задания
Заголовок раздела «Практические задания»- Реализуй простую RBAC систему с ролями USER/EDITOR/ADMIN
- Создай таблицы Role + Permission + UserRole в Prisma
- Напиши middleware
can('permission:name')для Express - Добавь UI компонент
<Can permission="...">для условного рендеринга