@cqrs-ddd/pipeline-casl
Εξουσιοδότηση λεπτομερούς ελέγχου βάσει χαρακτηριστικών (ABAC) και ρόλων (RBAC) χρησιμοποιώντας το CASL.
Επιβάλλει μοντέλο άμυνας σε βάθος δύο επιπέδων (two-tier defense-in-depth):
- Εξουσιοδότηση σε επίπεδο τύπου (Type-Level Outer Gate): Το
CaslBehaviorαξιολογεί στατικά δικαιώματα πριν την εκτέλεση της λειτουργίας (π.χ. Μπορεί ο χρήστης να εκτελέσει τοDeleteUserCommand;), αποφεύγοντας περιττά queries στη βάση. - Εξουσιοδότηση οντότητας & πεδίων (Entity & Field Inner Gate): Το
CaslAuthorizerαξιολογεί λεπτομερείς κανόνες πάνω σε ενυδατωμένα (hydrated) domain aggregates εντός του handler (π.χ. Μπορεί ο χρήστης να ενημερώσει το συγκεκριμένο άρθρο ότανauthorId !== currentUserId;) και προβάλλει μόνο αναγνώσιμα πεδία.
Εγκατάσταση
Ενότητα με τίτλο «Εγκατάσταση»pnpm add @cqrs-ddd/pipeline-casl @cqrs-ddd/pipeline @casl/abilityΑπαιτεί Node.js 22.12 ή νεότερο και @casl/ability 7.
Αρχιτεκτονική & Ροή εξουσιοδότησης
Ενότητα με τίτλο «Αρχιτεκτονική & Ροή εξουσιοδότησης»Εισερχόμενο Αίτημα (Incoming Request) │ ▼[1. Type-Level Gate: CaslBehavior] ├─ Επιλύει το principal και τους κανόνες μέσω ICaslPermissionSource ├─ Αξιολογεί τους κανόνες τύπου που δηλώθηκαν με requires({ action, subject }) ├─ Απόρριψη; ──► Ρίχνει UnauthorizedActionException (HTTP 403) └─ Επιτρέπεται ──► Αποθηκεύει Ability & Principal στο IPipelineContext │ ▼[2. Εκτέλεση Handler] ├─ Φορτώνει το domain aggregate από το repository (π.χ. post = await repo.findById(id)) ├─ Δημιουργεί CaslAuthorizer (διαβάζει το Ability από το context) ├─ Αξιολογεί authorizer.authorize('update', post, ['title']) ├─ Τροποποιεί το aggregate μέσω domain μεθόδων ├─ Αποθηκεύει το aggregate └─ Επιστρέφει προβληθέντα πεδία: authorizer.project('read', post, dto)Γρήγορο παράδειγμα
Ενότητα με τίτλο «Γρήγορο παράδειγμα»import { createPipeline } from '@cqrs-ddd/pipeline';import { CaslBehavior, type ICaslPermissionSource, parseCapabilityString, requires,} from '@cqrs-ddd/pipeline-casl';
const permissionSource: ICaslPermissionSource = { load: async (context) => { const session = await authService.getCurrentSession(); if (!session) return null; // Μη αυθεντικοποιημένα αιτήματα απορρίπτονται
return { principal: { id: session.userId, tenantId: session.tenantId, role: session.role }, rules: session.capabilities.map(parseCapabilityString), }; },};
const pipeline = createPipeline({ behaviors: [new CaslBehavior(permissionSource)], globalBehaviors: { before: [CaslBehavior] },});
export const deleteUser = pipeline.wrap( { name: 'deleteUser', kind: 'command' }, requires({ action: 'delete', subject: 'User' }),)(async (userId: string) => usersRepo.delete(userId));Μορφή Capability & Παρεμβολή συνθηκών (Interpolation)
Ενότητα με τίτλο «Μορφή Capability & Παρεμβολή συνθηκών (Interpolation)»Η πηγή δικαιωμάτων φορτώνει κανόνες ως αντικείμενα Capability. Για συμπαγή αποθήκευση στη βάση ή claims σε JWT, σειριοποιήστε τα ως capability strings:
Subject|action|conditions|fields| Capability String | Action | Subject | Conditions / Fields | Σημασία |
|---|---|---|---|---|
Post|read|* |
read |
Post |
Wildcard | Μπορεί να διαβάσει οποιοδήποτε post |
!Post|delete|* |
delete |
Post |
Inverted (!) |
Ρητή απαγόρευση διαγραφής posts |
Post|update|{"authorId":"${user.id}"}|title,body |
update |
Post |
${user.id} ταύτιση |
Ενημέρωση τίτλου και body μόνο σε δικά του posts |
all|manage|* |
manage |
all |
Wildcard | Superadmin: επιτρέπεται κάθε ενέργεια |
Placeholders της μορφής ${user.<field>} στις συνθήκες αντικαθίστανται αυτόματα από χαρακτηριστικά του ενεργού principal:
// Κανόνας: {"tenantId":"${user.tenantId}","department":"${user.dept}"}// Principal: { id: 'u_1', tenantId: 'org_abc', dept: 'engineering' }// Αντικατεστημένη συνθήκη: { tenantId: 'org_abc', department: 'engineering' }Έλεγχοι οντοτήτων και πεδίων (CaslAuthorizer)
Ενότητα με τίτλο «Έλεγχοι οντοτήτων και πεδίων (CaslAuthorizer)»Μέσα σε command ή query handlers, δημιουργήστε instance του CaslAuthorizer για να αξιολογήσετε κανόνες έναντι φορτωμένων οντοτήτων και να καθαρίσετε πεδία εξόδου:
import { CaslAuthorizer } from '@cqrs-ddd/pipeline-casl';
export class UpdateArticleHandler { async execute(command: UpdateArticleCommand) { const authorizer = new CaslAuthorizer(); const article = await this.articles.findById(command.articleId);
// 1. Εξουσιοδότηση ενέργειας και τροποποιημένων πεδίων στο aggregate: authorizer.authorize('update', article, ['title', 'content']);
// 2. Εκτέλεση domain mutation: article.updateContent(command.title, command.content); await this.articles.save(article);
// 3. Προβολή μόνο των πεδίων που επιτρέπεται να διαβάσει ο καλών: return authorizer.project('read', article, { id: article.id, title: article.title, content: article.content, internalNotes: article.internalNotes, }); }}Ασφάλεια & Διαχωρισμός Cache (abilityDigest)
Ενότητα με τίτλο «Ασφάλεια & Διαχωρισμός Cache (abilityDigest)»Όταν αποθηκεύετε αποτελέσματα query στην cache (@cqrs-ddd/pipeline-cache) ή εκτελείτε replay idempotent commands (@cqrs-ddd/pipeline-idempotency), οι απαντήσεις δεν πρέπει ποτέ να διαμοιράζονται μεταξύ διαφορετικών επιπέδων δικαιωμάτων.
Το abilityDigest(context) παράγει SHA-256 αποτύπωμα των κανόνων του καλούντος:
import { createPartitionedCacheKeyFactory } from '@cqrs-ddd/pipeline-cache';import { abilityDigest, getCaslPrincipal } from '@cqrs-ddd/pipeline-casl';
const userCacheKey = createPartitionedCacheKeyFactory({ principal: (ctx) => getCaslPrincipal(ctx)?.id, scope: abilityDigest,});Ενσωμάτωση NestJS (@cqrs-ddd/nestjs)
Ενότητα με τίτλο «Ενσωμάτωση NestJS (@cqrs-ddd/nestjs)»Καταχωρίστε το CaslBehavior σε ένα shared security module:
import { Module } from '@nestjs/common';import { CaslBehavior } from '@cqrs-ddd/pipeline-casl';import { AuthService } from '../auth/auth.service.js';
@Module({ providers: [ { provide: CaslBehavior, inject: [AuthService], useFactory: (auth: AuthService) => new CaslBehavior(auth.permissionSource), }, ], exports: [CaslBehavior],})export class SecurityModule {}Διακοσμήστε handlers με requires:
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';import { UsePipeline } from '@cqrs-ddd/pipeline';import { requires } from '@cqrs-ddd/pipeline-casl';
@CommandHandler(DeleteArticleCommand)@UsePipeline(requires({ action: 'delete', subject: 'Article' }))export class DeleteArticleHandler implements ICommandHandler<DeleteArticleCommand> { async execute(command: DeleteArticleCommand) {}}Το ErrorFilter μετατρέπει αυτόματα το UnauthorizedActionException σε HTTP 403 Forbidden.
Σειρά των behaviors
Ενότητα με τίτλο «Σειρά των behaviors»Τοποθετήστε το CaslBehavior πριν από behaviors cache και idempotency:
ZodValidationBehavior: Επικυρώνει το input.CaslBehavior: Ελέγχει την εξουσιοδότηση.CacheBehavior/IdempotencyBehavior: Αξιολογεί short-circuit keys μέσωabilityDigest.