ویرگول
ورودثبت نام
مهدی قوسیان | Mahdi Gosyan
مهدی قوسیان | Mahdi Gosyanبرنامه‌نویس از پشت با آچار تو دست و کتاب رو میز
مهدی قوسیان | Mahdi Gosyan
مهدی قوسیان | Mahdi Gosyan
خواندن ۵ دقیقه·۳ ماه پیش

خداحافظی با RBAC ساده و سلام به CASL در NestJS

اگر در حال توسعه یک اپلیکیشن جدی با NestJS هستید، احتمالاً با Decorator آشنای @Roles('admin') شروع کرده اید. این روش ساده و قشنگ است؛ اما فقط تا وقتی خوب است که پروژه تان کوچک باشد و تعداد نقش ها به چندتا محدود شود. بیایید صادق باشیم: در دنیای واقعی، امنیت به این سادگی نیست و «نقش ها» به تنهایی قادر به بیان پیچیدگی های دسترسی کاربران نیستند.

جایی که RBAC میشکند:
چند سناریوی واقعی را در نظر بگیرید:

  • کاربر باید بتواند پست خودش را ویرایش کند، اما نه پست دیگران را.

  • مدیر بخش باید بتواند مقالات «پیش نویس» را ببیند، اما نه مقالات «منتشرشده» را.

  • کاربر VIP باید به بخش نظرات دسترسی داشته باشد، اما فقط به شرطی که ظرف سی روز گذشته بیش از یک «اِخطار» دریافت نکرده باشد.

  • یک نفر باید بتواند فایلی را در پروژه اش حذف کند، اما دقیقاً تا ۲۴ ساعت پس از انتشارش، نه بیشتر و نه کمتر.

در تمام این مثالها، داشتن «نقش» کافی نیست؛ آنچه تعیین کننده است، ویژگی ها (Attributes) هستند: مالکیت، وضعیت، زمان، تعداد خطاها و… .

اینجاست که RBAC (Role-Based Access Control) شکست میخورد و پای ABAC (Attribute-Based Access Control) به میان میآید. و ابزار محبوب ما برای پیاده سازی این الگو در اکوسیستم NestJS، نبودِ کتابخانه ای بهتر از CASL است.

چرا CASL؟ (و نه یک Guard سفارشی دیگر):
خیلی از تیم ها به جای این کار، یک Guard مینویسند که داخلش صد خط if/else هست و مستقیماً با دیتابیس و Doctrine درگیر میشود. مشکل اینجاست که:

  1. منطق امنیت در گوشه وکنار Controllerها و Serviceها پخش میشود.

  2. تغییر یک قانون یعنی دست زدن به کدهایی که ربطی به امنیت ندارند.

  3. تست کردن همه ی این شاخه های شرطی، یک معجزه ی واقعی است.

CASL این مسائل را حل میکند. فلسفه ی آن ساده اما قدرتمند است: شما اجازه ها را به شکل «جمله» تعریف میکنید؛ همانطور که یک انسان اجازهاش را بیان میکند:
این یعنی منطق امنیت از بدنه ی بیزینس جدا میشود و در یک نقطه ی واحد، متمرکز و قابل آزمایش قرار میگیرد.

قدم اول: تعریف توانمندی ها (Ability Factory):
ابتدا باید مشخص کنیم هر موجودی (کاربر، سیستم، سرویس) چه کارهایی میتواند انجام دهد. یک AbilityFactory میسازیم؛ اینجا قلب تمام قوانین امنیتی ماست:

// ability.factory.ts import { Ability, AbilityBuilder, AbilityClass, ExtractSubjectType, InferSubjects } from '@casl/ability'; import { Injectable } from '@nestjs/common'; import { Action } from './action.enum'; import { Article } from '../articles/article.entity'; import { User } from './user.entity'; type Subjects = InferSubjects<typeof Article | typeof User> | 'all'; export type AppAbility = Ability<[Action, Subjects]>; @Injectable() export class AbilityFactory { defineAbility(user: User): AppAbility { const { can, cannot, build } = new AbilityBuilder<AppAbility>(Ability as AbilityClass<AppAbility>); if (user.isAdmin) { // ادمین همهکاره است، اما حتی او هم نمیتواند مقاله منتشرشده را پاک کند! can(Action.Manage, 'all'); cannot(Action.Delete, Article, { isPublished: true }); } else { // همهی کاربران عادی میتوانند بخوانند... can(Action.Read, 'all'); // ...مقالات خودشان را ویرایش کنند... can(Action.Update, Article, { authorId: user.id }); // ...و فقط پیشنویسهای خودشان را حذف کنند. can(Action.Delete, Article, { authorId: user.id, isPublished: false }); // و یک قانون ظریف: اگر کاربر آفلاین است، هیچ قابلیتی ندارد. if (user.status === 'suspended') { cannot(Action.Manage, 'all'); } } return build(); } }

دقت کنید: قوانین فقط «چه کسی چه کاری میتواند» را مشخص میکنند و هیچ ربطی به نحوه ی اجرای کار در دیتابیس ندارند. این یعنی در تست، میتوانید بدون هیچ دیتابیسی، تمام این قوانین را صرفاً با یک شیء User ساده بررسی کنید.

قدم دوم: Guard جادویی (The Shield):
حالا به جای اینکه در هر Controller کلی if/else بنویسیم، یک Guard میسازیم که از Metadatas استفاده میکند و قوانین را از Decorators میخواند:

// abilities.guard.ts import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AppAbility, AbilityFactory } from './ability.factory'; export const CHECK_ABILITY = 'check_ability'; export interface RequiredRule { action: string; subject: any; } @Injectable() export class AbilitiesGuard implements CanActivate { constructor( private reflector: Reflector, private abilityFactory: AbilityFactory, ) {} async canActivate(context: ExecutionContext): Promise<boolean> { const rules = this.reflector.getAllAndOverride<RequiredRule[]>(CHECK_ABILITY, [ context.getHandler(), context.getClass(), ]); if (!rules?.length) { return true; // بدون قانون، همهچیز آزاد (یا برای مسیرهای عمومی) } const { user } = context.switchToHttp().getRequest(); const ability = this.abilityFactory.defineAbility(user); const allowed = rules.every((rule) => ability.can(rule.action, rule.subject)); if (!allowed) { throw new ForbiddenException('شما مجاز به انجام این عملیات نیستید.'); } return allowed; } }

برای اینکه آرگومانها بتوانند dynamic باشند (مثلاً subject واقعی همان Recordای که کاربر میخواهد ویرایش کند)، میتوانید subject را از Req یا Param بخوانید و به CASL بدهید تا کل شرطها را روی آن اعمال کند – این همان کاری است که در اصلاحات انجام میشود و CASL به صورت درونی handle میکند.

قدم سوم: Decorator تمیز و استفاده در Controller
یک Decorator ساده میسازیم تا کد Controller شبیه یک شعر شود:

// permissions.decorator.ts import { SetMetadata } from '@nestjs/common'; export const CheckAbilities = (...rules: RequiredRule[]) => SetMetadata(CHECK_ABILITY, rules);

و استفادهی نهایی، تمیز و باکلاس:

// articles.controller.ts @UseGuards(JwtAuthGuard, AbilitiesGuard) @Controller('articles') export class ArticlesController { @Patch(':id') @CheckAbilities({ action: Action.Update, subject: Article }) async update(@Param('id') id: string) { // اگر کاربر اجازه نداشته باشد، قبل از رسیدن به اینجا 403 میگیرد. return this.articleService.update(id); } @Delete(':id') @CheckAbilities({ action: Action.Delete, subject: Article }) async remove(@Param('id') id: string) { return this.articleService.remove(id); } }

نکته ی طلایی: برای قوانین پویا (مثل «فقط مدرک خودش»)، subject را با داده ی به روز از دیتابیس به Guard تزریق کنید؛ اینطوری CASL همه ی شرط ها را روی همان شیء اجرا میکند و امنیت واقعاً دقیق میشود.

چرا این روش «خفن» است؟

۱.امنیت متمرکز: تمام قوانین دسترسی شما در یک فایل (AbilityFactory) جمع است، نه پخش شده در کل پروژه. هر تغییر، یک جا و قابل ردیابی.

۲. تست پذیری بالا: به راحتی میتوانید Unit Test بنویسید که فقط یک کاربر ساختگی و قوانین را چک کند، بدون اینکه با HTTP، دیتابیس یا Redis درگیر شوید.

۳. توسعه پذیری افسانه ای: اضافه کردن یک شرط جدید (مثل محدودیت زمانی ویرایش یا شرط تعداد اخطار) تنها چند خط در AbilityFactory است و نیازی به دستزدن به Controller یا Service نیست.

۴. حذف مسیرهای خطرناک: دیگر ناگهان یک گارد فراموش شده یا یک if/else گمشده در میان کدها وجود ندارد؛ همه چیز تحت پوشش یک سیستم واحد است.

من مهدی قوسیان هستم و خوشحال میشم نظراتتون بنویسین این پست رو به دوستاتون ارسال کنین.

nestjsjavascriptجاوااسکریپتnodejsdatabase
۳
۰
مهدی قوسیان | Mahdi Gosyan
مهدی قوسیان | Mahdi Gosyan
برنامه‌نویس از پشت با آچار تو دست و کتاب رو میز
شاید از این پست‌ها خوشتان بیاید