WisarWisar
Dasturlash kitobi/8-QISM — NestJS22 daqiqa

8.7-bob: User–Role–Auth, RBAC (ruxsatlarni boshqarish)

8-QISM — NestJS (chuqur) · 7-mavzu


1. Kirish va motivatsiya

Guards va enhancer mexanizmlarini 8.6-bob bildik. Endi ularni ishlatib, NestJS'da to'liq RBAC (Role-Based Access Control — rollarga asoslangan ruxsat) tizimini quramiz. 5.17-bobda RBAC'ni Express'da nazariy/amaliy ko'rdik; endi NestJS'da — guard, custom dekorator, ownership, va dinamik ruxsatlar bilan — professional darajada. Bu — har korporativ ilovaning markaziy qismi (admin, moderator, user — kim nimaga haqli).

5.17'dagi ikki savol takrorlanadi: authentication (8.9 — kim siz?) va authorization (bu bob — nimaga haqlisiz?). NestJS'da authorization — RolesGuard (8.6: 2.4) + @Roles() custom dekorator (8.6: 2.9) + Reflector (metadata o'qish — 8.6: 2.7). Bu — 5.17'dagi qo'lda middleware'ning NestJS'dagi deklarativ, toza versiyasi: @Roles("admin") yozasiz — guard avtomatik tekshiradi.

Bu bob promptdagidan ko'proq beradi: oddiy rol-asosli RBAC (rol ruxsat), permission-asosli (aniq ruxsatlar — 5.17: 2.10), ownership (o'z resursini — 5.17: 2.8, IDOR himoyasi — 14), va CASL (dinamik, attribut-asosli ruxsat — promptda yo'q, lekin professional NestJS RBAC'ning standarti). Bu bob: User-Role entity, RolesGuard, permission guard, ownership, va CASL — chuqur. Bu — auth 8.9-bob bilan birga ishlaydi.

O'xshatish (RBAC): RBAC — kompaniyadagi lavozimlar va kalitlar 5.17-bob. Xodimga lavozim beriladi (rol — admin/moderator/user); har lavozim muayyan xonalar kalitini oladi (ruxsatlar). NestJS'da @Roles("admin") — "bu eshik faqat admin kalitini qabul qiladi"; RolesGuard — eshik qulfini tekshiruvchi. Ownership — "o'z kabinetingizga kirasiz, boshqaning kabinetiga yo'q" (rol bir xil bo'lsa ham — 14). CASL — aqlli qulf ("o'z hujjatingizni tahrirlaysiz, lekin boshqanikini faqat o'qiysiz").

Nega muhim?

  • Korporativ markaz — admin/moderator/user — har ilovada.
  • Xavfsizlik (14) — kim nimaga haqli; ruxsatsiz amal — bloklanadi.
  • Auth davomi (8.9) — authentication'dan keyin authorization.
  • CASL — murakkab, dinamik ruxsat (professional NestJS).

2. Nazariya — chuqur tushuntirish

2.1. Authentication vs Authorization (NestJS'da — 5.17 takrori)

text
  Authentication 8.9-bob:  KIM siz?  AuthGuard (JWT — req.user)
  Authorization (bu bob): NIMAGA haqlisiz?  RolesGuard (rol/ruxsat)

  Tartib (8.6: 2.11): AuthGuard  RolesGuard
  (avval kim ekanini aniqla, keyin ruxsatni tekshir)

Authorization (bu bob) — authentication'dan (8.9) keyin. AuthGuard req.user ni qo'yadi 8.9-bob, RolesGuard uning rolini tekshiradi (5.17: 2.1). Guard tartibi muhim: @UseGuards(AuthGuard, RolesGuard) (8.6: 2.11).

2.2. User-Role modeli (entity)

ts
// Oddiy: rol — enum (user entity'da)
@Entity()
export class User {
  @PrimaryGeneratedColumn() id: number;
  @Column({ type: "enum", enum: ["user", "moderator", "admin"], default: "user" })
  rol: string;                                     // rol (5.17: 2.4)
}
text
  Murakkab: alohida Role/Permission entity (dinamik — 2.9)
  User N:M Role N:M Permission (DB'dan boshqariladi)

Rol modeli: oddiyrol enum (user entity'da — ko'p loyiha uchun yetadi). Murakkab — alohida Role + Permission entity (N:M — dinamik, admin paneldan boshqariladi — 2.9). Boshlash uchun enum; o'sgan sari permission-asosli 2.7-bob.

2.2a. User modul: entity, service, module (to'liq)

RBAC'ning poydevori — User entity va uni boshqaradigan service. Rol shu entity'da yashaydi 2.2-bob, auth 8.9-bob esa shu user'ni topib, token'ga rolni joylaydi. To'liq user modulini quramiz:

ts
// user.entity.ts (8.3 — TypeORM entity)
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Rol } from "./rol.enum";                    // (Misol 1)

@Entity("users")
export class User {
  @PrimaryGeneratedColumn() id: number;

  @Column({ unique: true }) email: string;           // login uchun (8.9)

  @Column({ select: false }) parolHash: string;      // select:false — so'rovlarda kelmaydi (14)

  @Column({ type: "enum", enum: Rol, default: Rol.USER })
  rol: Rol;                                           // RBAC markazi (2.2)

  @OneToMany(() => Order, (order) => order.user)
  buyurtmalar: Order[];                               // ownership uchun (2.6)
}
ts
// users.service.ts — user CRUD + rol boshqaruvi
import { Injectable, NotFoundException, ConflictException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";

@Injectable()
export class UsersService {
  constructor(@InjectRepository(User) private repo: Repository<User>) {}

  // auth 8.9-bob — email bo'yicha topish (parolHash bilan — addSelect)
  async emailBoyicha(email: string): Promise<User | null> {
    return this.repo
      .createQueryBuilder("u")
      .addSelect("u.parolHash")                       // select:false ni ochamiz (14)
      .where("u.email = :email", { email })
      .getOne();
  }

  async bitta(id: number): Promise<User> {
    const user = await this.repo.findOne({ where: { id } });
    if (!user) throw new NotFoundException("Foydalanuvchi topilmadi");
    return user;
  }

  async yarat(email: string, parolHash: string): Promise<User> {
    const bor = await this.repo.findOne({ where: { email } });
    if (bor) throw new ConflictException("Bu email band");   // 409
    const user = this.repo.create({ email, parolHash });     // rol — default USER (2.2)
    return this.repo.save(user);
  }

  // rol tayinlash — faqat admin chaqiradi (Misol 9)
  async rolTayinla(id: number, rol: Rol): Promise<User> {
    const user = await this.bitta(id);
    user.rol = rol;
    return this.repo.save(user);
  }
}
ts
// users.module.ts — modul (8.2)
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";

@Module({
  imports: [TypeOrmModule.forFeature([User])],       // repository DI (8.3)
  providers: [UsersService],
  exports: [UsersService],                           // AuthModule 8.9-bob ishlatadi
})
export class UsersModule {}

User modul — RBAC'ning poydevori. Entity (rol ustuni — 2.2; parolHash select:false bilan yashirin — 14; buyurtmalar relation — ownership uchun 2.6). ServiceemailBoyicha (auth login — 8.9, parolHash'ni addSelect bilan ochadi), yarat, rolTayinla (admin — Misol 9). ModuleUsersService'ni export qiladi (AuthModule — 8.9 — import qilib ishlatadi). Bu modul — auth 8.9-bob va RBAC (bu bob) uchun umumiy asos.

2.3. @Roles dekorator + RolesGuard (asosiy — 8.6 takrori)

ts
// roles.decorator.ts (8.6: 2.9)
export const Roles = (...roles: string[]) => SetMetadata("roles", roles);

// roles.guard.ts (8.6: 2.7)
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {
    const kerakli = this.reflector.getAllAndOverride<string[]>("roles", [
      context.getHandler(), context.getClass(),
    ]);
    if (!kerakli) return true;                       // rol talab qilinmagan
    const { user } = context.switchToHttp().getRequest();
    return kerakli.includes(user.rol);               // RBAC (5.17)
  }
}

// Ishlatish
@UseGuards(AuthGuard, RolesGuard)
@Roles("admin")
@Delete(":id")
ochir() {}

Rol-asosli RBAC (8.6'da ko'rdik): @Roles("admin") (metadata) + RolesGuard (Reflector o'qiydi). AuthGuard, RolesGuard (tartib — auth birinchi — 2.1). Bu — eng oddiy, keng ishlatiladigan RBAC. Ko'p loyiha uchun yetadi.

getAllAndOverride nima uchun? (8.6: 2.7) — metadata ikki joyda bo'lishi mumkin: handler'da (metod ustidagi @Roles) va class'da (controller ustidagi @Roles). getAllAndOverride<T>(key, [getHandler(), getClass()]) — massivni tartib bilan tekshiradi va birinchi topilganini qaytaradi (handler class'ni ustidan bosadi — override). Ya'ni: controller @Roles(ADMIN) bo'lsa, lekin bitta metodda @Roles(USER) bo'lsa — o'sha metodda USER amal qiladi. Buning muqobili — getAllAndMerge (ikkalasini birlashtiradi, override qilmaydi) yoki oddiy get (faqat bitta target'dan o'qiydi — 2.4, 2.5'da). Rollar uchun getAllAndOverride mantiqiy: aniqroq (handler) darajadagi qoida umumiyni (class) bekor qiladi.

2.4. Rol iyerarxiyasi (NestJS'da — 5.17: 2.7)

ts
const ROL_DARAJA = { user: 1, moderator: 2, admin: 3, superadmin: 4 };

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {
    const minRol = this.reflector.get<string>("minRole", context.getHandler());
    if (!minRol) return true;
    const { user } = context.switchToHttp().getRequest();
    return ROL_DARAJA[user.rol] >= ROL_DARAJA[minRol];   // daraja (5.17: 2.7)
  }
}
// @MinRole("moderator")  moderator va yuqori (admin, superadmin) o'tadi

Rol iyerarxiyasi (5.17: 2.7) — yuqori rol quyi ruxsatni oladi (admin > moderator > user). NestJS'da — daraja taqqoslash guard'da. @MinRole("moderator") — moderator va yuqori. Tekis rol (faqat aniq rol) yoki iyerarxik — loyihaga qarab.

2.5. Permission-asosli RBAC (aniq ruxsatlar — 5.17: 2.10)

ts
// Rol  ruxsatlar (5.17: 2.10)
const ROL_RUXSAT: Record<string, string[]> = {
  user: ["buyurtma:yaratish", "profil:o'qish"],
  moderator: ["sharh:o'chirish", "buyurtma:o'qish"],
  admin: ["*"],                                     // hammasi
};

export const Permissions = (...p: string[]) => SetMetadata("permissions", p);

@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {
    const kerakli = this.reflector.get<string[]>("permissions", context.getHandler());
    if (!kerakli) return true;
    const { user } = context.switchToHttp().getRequest();
    const ruxsatlar = ROL_RUXSAT[user.rol] || [];
    return ruxsatlar.includes("*") || kerakli.every((p) => ruxsatlar.includes(p));
  }
}
// @Permissions("sharh:o'chirish")

Permission-asosli (5.17: 2.10) — rol emas, aniq ruxsat (@Permissions("sharh:o'chirish")). Moslashuvchanroq: yangi ruxsat qo'shish oson (rolga permission qo'shasiz, kodga tegmaysiz). Murakkabroq, lekin nozik. Katta loyihada afzal.

2.6. Ownership (o'z resursi — IDOR himoyasi — 14)

Rol yetarli emas: foydalanuvchi o'z resursini o'zgartirishi mumkin, boshqaning emas (5.17: 2.8):

ts
// Ownership service'da tekshiriladi (guard'da resurs yo'q)
@Injectable()
export class OrdersService {
  async ochir(orderId: number, user: User): Promise<void> {
    const order = await this.repo.findOne({ where: { id: orderId } });
    if (!order) throw new NotFoundException();

    // OWNERSHIP: o'ziniki yoki admin (5.17: 2.8, 14)
    if (order.userId !== user.id && user.rol !== "admin") {
      throw new ForbiddenException("Bu buyurtma sizniki emas");   // 403 (IDOR — 14)
    }
    await this.repo.delete(orderId);
  }
}

Ownership (5.17: 2.8, 14): rol bir xil bo'lsa ham, foydalanuvchi boshqaning resursini o'zgartira olmasligi kerak (IDOR hujumi — 14). Guard'da resurs yo'q (DB so'rovsiz) service'da tekshiriladi (order.userId === user.id). Yoki ownership guard (DB bilan — 2.7). Eng ko'p e'tibordan chetda qoladigan, lekin muhim.

2.7. Ownership guard (qayta ishlatiladigan)

ts
// Ownership guard (DB bilan — 5.17: Misol 4)
@Injectable()
export class OwnershipGuard implements CanActivate {
  constructor(private dataSource: DataSource, private reflector: Reflector) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const entityClass = this.reflector.get("ownerEntity", context.getHandler());
    const request = context.switchToHttp().getRequest();
    const { user } = request;
    const id = request.params.id;

    if (user.rol === "admin") return true;            // admin — hammasi
    const resurs = await this.dataSource.getRepository(entityClass).findOne({ where: { id } });
    if (!resurs) throw new NotFoundException();
    if (resurs.userId !== user.id) throw new ForbiddenException("Ruxsat yo'q");   // (14)
    return true;
  }
}
// @CheckOwnership(Order) @UseGuards(AuthGuard, OwnershipGuard)

Ownership guard — qayta ishlatiladigan (DB'dan resurs olib, egasini tekshiradi). Lekin guard'da DB so'rovi (qo'shimcha) — ba'zan service'da tekshirish 2.6-bob soddaroq (resurs baribir kerak). Tanlov: takroriy ownership guard; bir martalik service.

2.8. CASL — dinamik, attribut-asosli (promptda yo'q — professional)

CASL — NestJS'da murakkab ruxsatning standart kutubxonasi (docs.nestjs.com — authorization):

text
  CASL nima:
  - "ability" — kim nima qila oladi (can/cannot)
  - Attribut-asosli: shartlar bilan ("o'z postini tahrirlaydi")
  - Dinamik: runtime'da baholanadi (resurs ma'lumotiga qarab)
  - RBAC + ownership + ABAC — hammasi bir joyda

  RBAC vs CASL:
  RBAC — sodda (rol  ruxsat); katta loyihada cheklangan
  CASL — moslashuvchan (shart, ownership, attribut); murakkabroq

CASL (promptda yo'q, lekin professional NestJS RBAC standarti — docs): nafaqat "admin'mi?", balki "bu postni tahrirlay oladimi?" (ownership + shart bir joyda). ability.can("update", post) — post egasi bo'lsa true. Murakkab loyihada (granular ruxsat) — CASL. Oddiy loyihada — RolesGuard 2.3-bob.

2.9. CASL ability (ta'rif)

ts
import { AbilityBuilder, PureAbility } from "@casl/ability";

// Foydalanuvchiga qarab ability (kim nima qila oladi)
function defineAbility(user: User) {
  const { can, cannot, build } = new AbilityBuilder(PureAbility);

  if (user.rol === "admin") {
    can("manage", "all");                            // admin — hammasi
  } else {
    can("read", "Post");                             // har kim post o'qiydi
    can("create", "Post");
    can("update", "Post", { authorId: user.id });    // FAQAT o'z postini (ownership + shart!)
    can("delete", "Post", { authorId: user.id });
  }
  return build();
}

// Tekshirish
const ability = defineAbility(user);
ability.can("update", post);                          // post.authorId === user.id bo'lsa true

CASL abilitycan("update", "Post", { authorId: user.id }) — "o'z postini yangilaydi" (action + subject + shart). Ownership (authorId) ability ichida (alohida tekshiruv emas). ability.can("update", post) — post egasi bo'lsa true. Bu — RBAC + ownership + ABAC birga (8.dynamic).

2.10. CASL guard (dinamik)

ts
// CASL guard + dekorator
export const CheckPolicies = (...handlers) => SetMetadata("policies", handlers);

@Injectable()
export class PoliciesGuard implements CanActivate {
  constructor(private reflector: Reflector, private caslFactory: CaslAbilityFactory) {}

  canActivate(context: ExecutionContext): boolean {
    const policies = this.reflector.get("policies", context.getHandler()) || [];
    const { user } = context.switchToHttp().getRequest();
    const ability = this.caslFactory.createForUser(user);   // ability (2.9)
    return policies.every((handler) => handler(ability));    // shartlar
  }
}
// @CheckPolicies((ability) => ability.can("update", "Post"))

CASL guard — ability'ni yaratib, policy'ni tekshiradi (ability.can(...)). Dekorator (@CheckPolicies) bilan deklarativ. Murakkab, lekin kuchli (har endpoint uchun aniq ruxsat — shart bilan). Prisma bilan ham (query-level — manba). Katta loyihada professional yechim.

2.11. RBAC vs CASL (tanlov)

text
  RBAC (RolesGuard — 2.3):
   Sodda, tushunarli (rol  ruxsat)
   Ko'p loyiha uchun yetarli
   Murakkab/dinamik ruxsatda cheklangan (ownership alohida)

  CASL 2.9-bob:
   Granular (action + subject + shart)
   Ownership/attribut bir joyda
   Dinamik (runtime, DB'dan)
   Murakkabroq, o'rganish

   Oddiy: RolesGuard; murakkab/granular: CASL

Tanlov: RolesGuard 2.3-bob — oddiy, ko'p loyiha (admin/user). CASL 2.9-bob — murakkab, granular (o'z postini tahrirlash, shart bilan). Boshlash uchun RolesGuard; ehtiyoj o'sganda CASL. Ikkalasi ham NestJS'da standart.

2.12. nest-access-control (kutubxona)

text
  nest-access-control (nestjsx):
  - Role + Attribute based (RBAC + ABAC)
  - Grant'lar (kim, nima, qaysi resursga)
  - @UseRoles dekorator

   CASL muqobili; ba'zi loyihalarda

nest-access-control — RBAC/ABAC kutubxonasi (CASL muqobili). Grant-asosli (rol resurs amal). Tanlov: CASL (mashhurroq, isomorphic) yoki nest-access-control. Ko'p loyiha custom RolesGuard 2.3-bob yoki CASL'ni tanlaydi.

2.13. To'liq auth + RBAC oqimi

text
  So'rov  AuthGuard (8.9 — JWT  req.user) 
           RolesGuard (rol tekshir — 2.3) 
           [Service: ownership — 2.6] 
           Handler

  Custom dekoratorlar:
  @Public() — auth istisno (8.6: Misol 10)
  @Roles("admin") — rol 2.3-bob
  @CurrentUser() — req.user (8.6: 2.9)

To'liq oqim: AuthGuard (kim — 8.9) RolesGuard (rol — 2.3) service ownership 2.6-bob handler. Dekoratorlar (@Public, @Roles, @CurrentUser) — deklarativ. Bu — NestJS auth+RBAC'ning to'liq rasmi (8.9 bilan birga).

2.13a. @CurrentUser custom dekorator (param decorator — 8.6: 2.9)

Ownership 2.6-bob va handler'larda req.user'ni toza olish uchun param dekorator yozamiz. @Req() req req.user o'rniga to'g'ridan-to'g'ri @CurrentUser() user:

ts
// current-user.decorator.ts (8.6: 2.9 — createParamDecorator)
import { createParamDecorator, ExecutionContext } from "@nestjs/common";

export const CurrentUser = createParamDecorator(
  (data: keyof User | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;                        // AuthGuard qo'ygan (8.9, 2.1)
    return data ? user?.[data] : user;                // @CurrentUser("id") — bitta maydon
  },
);
ts
// Ishlatish — toza (req.user o'rniga)
@Get("profil")
@UseGuards(AuthGuard)
profil(@CurrentUser() user: User) {                   // to'liq user obyekti
  return user;
}

@Post("buyurtma")
@UseGuards(AuthGuard)
yarat(@CurrentUser("id") userId: number, @Body() dto: CreateOrderDto) {
  return this.ordersService.yarat(userId, dto);       // faqat id kerak bo'lsa
}

@CurrentUser (8.6: 2.9 — createParamDecorator) — req.user'ni handler parametrida toza oladi. AuthGuard (8.9) req.user'ni qo'yadi 2.1-bob, bu dekorator uni chiqarib beradi. data argument bilan bitta maydonni ham olish mumkin: @CurrentUser("id") userId: number. Bu — @Req() req + req.user'dan toza va tip-xavfsiz; ownership (2.6, Misol 4) va profil endpoint'larida doim ishlatiladi. Guard'siz (AuthGuard'siz) user undefined bo'lishini unutmang — shuning uchun @UseGuards(AuthGuard) bilan birga.

2.14. Best practices (RBAC — 14)

text
   AuthGuard  RolesGuard tartibi (auth birinchi — 2.1)
   Rol enum (oddiy) yoki Permission entity (dinamik — 2.2)
   Ownership tekshir (rol yetarli emas — IDOR — 2.6, 14)
   Deny by default (ruxsat aniq berilmasa — yo'q — 5.17: 2.11)
   Murakkab  CASL (granular, shart — 2.9)
   @Public global guard istisno 8.6-bob
   Frontend yashirish — UX; backend — HAQIQIY himoya (14)
   Rol/ruxsatni token'da 8.9-bob yoki DB'dan (sezgir — 5.17: 2.4)

3. Sintaksis — tez ma'lumotnoma

ts
// Rol-asosli (2.3)
@Roles("admin") @UseGuards(AuthGuard, RolesGuard) @Delete(":id") ochir() {}

// Permission-asosli (2.5)
@Permissions("sharh:o'chirish") @UseGuards(AuthGuard, PermissionsGuard)

// Ownership 2.6-bob: service'da order.userId === user.id || user.rol === "admin"

// CASL (2.9, 2.10)
@CheckPolicies((ability) => ability.can("update", "Post"))
@UseGuards(AuthGuard, PoliciesGuard)

4. Batafsil kod namunalari

Misol 1 — To'liq RBAC modul (rol enum — 2.2, 2.3)

ts
// rol.enum.ts
export enum Rol {
  USER = "user",
  MODERATOR = "moderator",
  ADMIN = "admin",
}

// roles.decorator.ts (8.6: 2.9)
import { SetMetadata } from "@nestjs/common";
export const ROLES_KEY = "roles";
export const Roles = (...roles: Rol[]) => SetMetadata(ROLES_KEY, roles);

// roles.guard.ts (8.6: 2.7)
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const kerakliRollar = this.reflector.getAllAndOverride<Rol[]>(ROLES_KEY, [
      context.getHandler(), context.getClass(),
    ]);
    if (!kerakliRollar?.length) return true;          // rol talab qilinmagan

    const { user } = context.switchToHttp().getRequest();
    if (!user) throw new ForbiddenException("Avtorizatsiya kerak");

    const hasRole = kerakliRollar.includes(user.rol);
    if (!hasRole) throw new ForbiddenException("Bu amal uchun ruxsatingiz yo'q");   // 403
    return true;
  }
}

Misol 2 — Controller (rollar bilan — 2.3, 2.13)

ts
@Controller("products")
@UseGuards(AuthGuard, RolesGuard)                    // butun controller (8.6: 2.10)
export class ProductsController {
  constructor(private productsService: ProductsService) {}

  @Get()                                             // hamma (rol talab qilinmagan)
  hammasi() {
    return this.productsService.hammasi();
  }

  @Post()
  @Roles(Rol.ADMIN)                                  // faqat admin (2.3)
  yarat(@Body() dto: CreateProductDto) {
    return this.productsService.yarat(dto);
  }

  @Delete(":id")
  @Roles(Rol.ADMIN, Rol.MODERATOR)                   // admin yoki moderator
  ochir(@Param("id", ParseIntPipe) id: number) {
    return this.productsService.ochir(id);
  }
}

Misol 3 — Permission-asosli (2.5)

ts
// permissions (rol  ruxsatlar — 5.17: 2.10)
export const RUXSATLAR: Record<Rol, string[]> = {
  [Rol.USER]: ["product:read", "order:create", "order:read:own"],
  [Rol.MODERATOR]: ["product:read", "review:delete", "order:read"],
  [Rol.ADMIN]: ["*"],
};

export const Permissions = (...p: string[]) => SetMetadata("permissions", p);

@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {
    const kerakli = this.reflector.getAllAndOverride<string[]>("permissions", [
      context.getHandler(), context.getClass(),
    ]);
    if (!kerakli?.length) return true;
    const { user } = context.switchToHttp().getRequest();
    const ruxsatlar = RUXSATLAR[user.rol] || [];
    const ruxsat = ruxsatlar.includes("*") || kerakli.every((p) => ruxsatlar.includes(p));
    if (!ruxsat) throw new ForbiddenException("Ruxsat yo'q");
    return true;
  }
}

// Ishlatish
@Permissions("review:delete")
@UseGuards(AuthGuard, PermissionsGuard)
@Delete("reviews/:id")
ochirSharh() {}

Misol 4 — Ownership (service'da — 2.6, 14)

ts
@Injectable()
export class OrdersService {
  constructor(@InjectRepository(Order) private repo: Repository<Order>) {}

  async bitta(id: number, user: User): Promise<Order> {
    const order = await this.repo.findOne({ where: { id }, relations: { user: true } });
    if (!order) throw new NotFoundException("Buyurtma topilmadi");

    // OWNERSHIP — o'ziniki yoki admin (5.17: 2.8, 14)
    if (order.user.id !== user.id && user.rol !== Rol.ADMIN) {
      throw new ForbiddenException("Bu buyurtma sizniki emas");   // IDOR himoyasi (14)
    }
    return order;
  }

  async ochir(id: number, user: User): Promise<void> {
    await this.bitta(id, user);                       // ownership tekshiradi (qayta ishlatish)
    await this.repo.delete(id);
  }
}

// Controller — @CurrentUser bilan (8.6: 2.9)
@Get(":id")
@UseGuards(AuthGuard)
bitta(@Param("id", ParseIntPipe) id: number, @CurrentUser() user: User) {
  return this.ordersService.bitta(id, user);
}

Misol 5 — Rol iyerarxiyasi (2.4)

ts
const ROL_DARAJA: Record<Rol, number> = {
  [Rol.USER]: 1,
  [Rol.MODERATOR]: 2,
  [Rol.ADMIN]: 3,
};

export const MinRole = (rol: Rol) => SetMetadata("minRole", rol);

@Injectable()
export class MinRoleGuard implements CanActivate {
  constructor(private reflector: Reflector) {}
  canActivate(context: ExecutionContext): boolean {
    const minRol = this.reflector.get<Rol>("minRole", context.getHandler());
    if (!minRol) return true;
    const { user } = context.switchToHttp().getRequest();
    if (ROL_DARAJA[user.rol] < ROL_DARAJA[minRol]) {
      throw new ForbiddenException("Ruxsat yo'q");
    }
    return true;
  }
}
// @MinRole(Rol.MODERATOR)  moderator va admin o'tadi (5.17: 2.7)

Misol 6 — CASL ability factory (2.9 — promptda yo'q)

ts
import { AbilityBuilder, PureAbility, ExtractSubjectType } from "@casl/ability";
import { Injectable } from "@nestjs/common";

type Action = "manage" | "create" | "read" | "update" | "delete";
type Subjects = "Post" | "Order" | "User" | "all";

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: User) {
    const { can, cannot, build } = new AbilityBuilder<PureAbility<[Action, Subjects]>>(PureAbility);

    if (user.rol === Rol.ADMIN) {
      can("manage", "all");                           // admin — hammasi (2.9)
    } else {
      can("read", "Post");                            // o'qish — hamma
      can("create", "Post");
      can("update", "Post", { authorId: user.id } as any);   // O'Z postini (ownership + shart!)
      can("delete", "Post", { authorId: user.id } as any);
      cannot("delete", "Post", { published: true } as any);  // e'lon qilinganni emas
    }

    return build({
      detectSubjectType: (item) => item.constructor.name as ExtractSubjectType<Subjects>,
    });
  }
}

Misol 7 — CASL guard + policy (2.10)

ts
// policies decorator
type PolicyHandler = (ability: any) => boolean;
export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata("policies", handlers);

@Injectable()
export class PoliciesGuard implements CanActivate {
  constructor(private reflector: Reflector, private caslFactory: CaslAbilityFactory) {}

  canActivate(context: ExecutionContext): boolean {
    const policies = this.reflector.get<PolicyHandler[]>("policies", context.getHandler()) || [];
    const { user } = context.switchToHttp().getRequest();
    const ability = this.caslFactory.createForUser(user);   // (2.9, Misol 6)
    return policies.every((handler) => handler(ability));
  }
}

// Ishlatish (deklarativ — 2.10)
@UseGuards(AuthGuard, PoliciesGuard)
@CheckPolicies((ability) => ability.can("create", "Post"))
@Post()
yaratPost() {}
ts
// casl.module.ts — CaslAbilityFactory'ni DI'ga ulash (8.2)
import { Module } from "@nestjs/common";

@Module({
  providers: [CaslAbilityFactory],                    // Misol 6
  exports: [CaslAbilityFactory],                      // PoliciesGuard va service'lar (Misol 8) import qiladi
})
export class CaslModule {}
// Ishlatuvchi modul: @Module({ imports: [CaslModule] }) — endi factory inject qilinadi

CaslModuleCaslAbilityFactory'ni (Misol 6) provider qilib, export qiladi. PoliciesGuard (Misol 7) va PostsService (Misol 8) uni inject qiladi — shuning uchun ularni ishlatadigan modul imports: [CaslModule] yozadi. Global guard (Misol 10) uchun ham factory shu modul orqali DI'ga tushadi.

Misol 8 — CASL service'da (ownership shart — 2.9)

ts
// Murakkab ownership — CASL bilan (2.9)
@Injectable()
export class PostsService {
  constructor(
    @InjectRepository(Post) private repo: Repository<Post>,
    private caslFactory: CaslAbilityFactory,
  ) {}

  async yangila(id: number, dto: UpdatePostDto, user: User): Promise<Post> {
    const post = await this.repo.findOne({ where: { id } });
    if (!post) throw new NotFoundException();

    const ability = this.caslFactory.createForUser(user);   // (2.9)
    if (!ability.can("update", post)) {                      // post.authorId tekshiriladi!
      throw new ForbiddenException("Bu postni tahrirlay olmaysiz");   // (14)
    }
    Object.assign(post, dto);
    return this.repo.save(post);
  }
}
// CASL — ownership + shart bir tekshiruvda (ability.can(post) — post atributlari)

Misol 9 — Rol tayinlash (admin — 5.17: Misol 8)

ts
@Controller("admin/users")
@UseGuards(AuthGuard, RolesGuard)
@Roles(Rol.ADMIN)                                    // butun controller — admin
export class AdminUsersController {
  constructor(private usersService: UsersService) {}

  @Patch(":id/role")
  rolTayinla(@Param("id", ParseIntPipe) id: number, @Body() dto: AssignRoleDto) {
    // dto.rol — enum validatsiya (8.5: @IsEnum)
    return this.usersService.rolTayinla(id, dto.rol);
  }
}

// DTO
export class AssignRoleDto {
  @IsEnum(Rol, { message: "Noto'g'ri rol" })          // (8.5)
  rol: Rol;
}

Misol 10 — Global RBAC sozlash (8.6: 2.12)

ts
// app.module.ts — global guard'lar (DI — 8.6: 2.12)
@Module({
  providers: [
    { provide: APP_GUARD, useClass: AuthGuard },     // 1. global auth (8.9)
    { provide: APP_GUARD, useClass: RolesGuard },    // 2. global roles (tartib — 2.1)
    CaslAbilityFactory,
  ],
})
export class AppModule {}
// Endi @Public() — auth istisno; @Roles() — rol; default — auth talab qilinadi
// Public endpoint: @Public() @Post("login") (8.6: Misol 10)

5. To'g'ri va noto'g'ri holatlar

1) Faqat rol, ownership'siz

text
 "user roli bor"  har buyurtmani o'chiradi (IDOR — 14, 2.6)
 ownership tekshir (order.userId === user.id)

2) Guard tartibi (roles auth'dan oldin)

ts
//  roles avval (req.user yo'q)
@UseGuards(RolesGuard, AuthGuard)

//  auth birinchi (2.1)
@UseGuards(AuthGuard, RolesGuard)

3) Frontend'ga ishonish

text
 "admin tugmasini yashirdim" (hacker API'ga — 14, 5.17: 2.11)
 backend guard (haqiqiy himoya)

4) Default ruxsat berish

ts
//  noma'lum rolga ruxsat (14)
if (user.rol === "banned") return false; return true;   // qolganlarga ruxsat!

//  deny by default (faqat ruxsat etilganlar)
return kerakliRollar.includes(user.rol);

5) Murakkab ruxsatni RolesGuard'da

text
 "o'z postini tahrirlash" RolesGuard'da (cheklangan — 2.11)
 CASL (ownership + shart — 2.9)

6. Keng tarqalgan xatolar va yechimlari

Xato 1 — Cannot read 'rol' of undefined

Sababi: req.user yo'q (AuthGuard ishlamagan yoki tartib — 2.1). Yechimi: AuthGuard RolesGuard'dan oldin.

Xato 2 — Admin ham 403 oladi

Sababi: rol nomi mos emas (enum) yoki token'da rol yo'q 2.2-bob. Yechimi: enum izchil; token payload'da rol 8.9-bob.

Xato 3 — Ownership ishlamaydi

Sababi: service'da tekshirilmagan 2.6-bob. Yechimi: order.userId === user.id || admin.

Xato 4 — CASL ability subject aniqlamaydi

Sababi: detectSubjectType yo'q 2.9-bob. Yechimi: detectSubjectType (constructor.name).

Xato 5 — Global guard hamma joyni bloklaydi

Sababi: @Public istisnosi yo'q (8.6: 2.9). Yechimi: @Public + guard tekshiruvi.

Xato 6 — Rol o'zgartirildi, eski token amal qiladi

Sababi: token'da eski rol (5.17: 2.4). Yechimi: qisqa token + refresh 5.16-bob; yoki DB'dan rol.


7. Integratsiya — bu mavzu stack'ning qayerida uchraydi

  • RBAC 5.17-bob: Express'dagi RBAC — NestJS'da.
  • Guards 8.6-bob: RolesGuard, PoliciesGuard.
  • Auth (8.9, 5.15): AuthGuard — req.user.
  • Custom decorator (8.6: 2.9): @Roles, @CurrentUser.
  • DTO 8.5-bob: rol validatsiya (@IsEnum).
  • DB 8.3-bob: User/Role entity.
  • Ownership (5.17: 2.8): IDOR himoyasi (14).
  • CASL: dinamik ruxsat (promptda yo'q).
  • Xavfsizlik (14): broken access control (OWASP).

8. Eng yaxshi amaliyotlar (best practices)

  • AuthGuard RolesGuard tartibi (auth birinchi — 2.1).
  • Rol enum (oddiy) yoki Permission entity (dinamik — 2.2, 2.5).
  • Ownership tekshir (rol yetarli emas — IDOR — 2.6, 14).
  • Deny by default (ruxsat aniq berilmasa — yo'q — 14).
  • Murakkab CASL (granular, shart, ownership — 2.9).
  • @Public (global guard istisno — 8.6).
  • Frontend yashirish — UX; backend — haqiqiy himoya (14).
  • @CurrentUser (toza req.user — 8.6).
  • Rol validatsiya (@IsEnum — DTO — 8.5).
  • Rol tayinlashni himoyala (faqat admin — Misol 9).

9. Amaliy loyiha: "To'liq RBAC Tizimi (NestJS)"

NestJS RBAC'ni mustahkamlash.

Maqsad

NestJS'da to'liq RBAC tizimini qurish: rol-asosli, permission-asosli, ownership, va CASL (granular).

Talablar (requirements)

  1. User-Role: rol enum (yoki entity); token'da rol (8.9 — Misol 1, 2.2).
  2. RolesGuard + @Roles: rol-asosli; auth tartibi (Misol 1, 2, 2.3).
  3. Rol iyerarxiyasi: @MinRole (Misol 5, 2.4).
  4. Permission-asosli: @Permissions + guard (Misol 3, 2.5).
  5. Ownership: service'da (o'z resursi — Misol 4, 2.6, 14).
  6. CASL (bonus): ability factory + PoliciesGuard (granular — Misol 6, 7, 8, 2.9).
  7. Rol tayinlash: faqat admin (Misol 9).
  8. Global RBAC: APP_GUARD + @Public (Misol 10, 8.6).
  9. Deny by default: ruxsat aniq berilmasa — yo'q (14).
  10. DTO: rol validatsiya (@IsEnum — 8.5).

Maslahatlar (hint)

  • Auth roles tartibi (2.1, 2-xato).
  • Ownership: service'da (rol yetarli emas — 2.6, 1-xato).
  • CASL: can(action, subject, { shart }) 2.9-bob.
  • @Public global istisno (8.6, 5-xato).
  • Deny by default (14, 4-xato).
  • @CurrentUser (8.6: 2.9).

"Tayyor" mezonlari (acceptance criteria)

  • User-Role (rol token'da).
  • RolesGuard + @Roles (tartib to'g'ri).
  • Rol iyerarxiyasi (@MinRole).
  • Permission-asosli.
  • Ownership (IDOR himoyasi).
  • (Bonus) CASL (granular, shart).
  • Rol tayinlash (admin).
  • Global RBAC + @Public.
  • Deny by default.
  • Rol validatsiya (@IsEnum).

Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.


10. Xulosa va keyingi bobga ko'prik

Bu bobda NestJS'da to'liq RBAC'ni o'rgandik:

  • Authentication vs Authorization (8.9 vs bu bob — 2.1); User-Role modeli (enum/entity — 2.2); User modul (entity/service/module — 2.2a); @CurrentUser (param dekorator — 2.13a).
  • Rol-asosli (@Roles + RolesGuard — 2.3); iyerarxiya (@MinRole — 2.4); permission-asosli (@Permissions — 2.5).
  • Ownership (o'z resursi — IDOR himoyasi — 2.6, 14); ownership guard 2.7-bob.
  • CASL (dinamik, attribut-asosli — ability + policy — 2.9, 2.10); RBAC vs CASL 2.11-bob; to'liq oqim 2.13-bob.

Keyingi bob — 8.8-bob: File upload, Swagger. RBAC'ni bildik; endi ikki amaliy mavzuni — NestJS'da fayl yuklash (FileInterceptor — Multer ustida — 5.11) va Swagger (API hujjat — 5.23) — o'rganamiz. Fayl yuklash va avtomatik hujjat — har real API'da kerak.


Foydalanilgan rasmiy/ishonchli manbalar

  • docs.nestjs.com/security/authorization (RBAC, claims, CASL integration)
  • DEV / Medium — Role-Based Access Control in NestJS; CASL vs RBAC 2026
  • github.com/nestjsx/nest-access-control; @casl/ability (dinamik ruxsat)

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
8.7-bob: User–Role–Auth, RBAC (ruxsatlarni boshqarish) — Wisar