-
Notifications
You must be signed in to change notification settings - Fork 0
FR NestJS Integration
@kengela/nestjs branche le PDP dans NestJS : un guard deny-by-default, des décorateurs pour
déclarer l'accès requis, un jeton d'injection pour le PDP, et une exception dédiée au step-up.
@nestjs/common,@nestjs/coreetreflect-metadatasont des peerDependencies : c'est votre application qui les fournit.
npm add @kengela/nestjs @kengela/authz-core @kengela/adapter-expr-cel| Export | Type | Rôle |
|---|---|---|
KengelaAuthzGuard |
guard | Deny-by-default ; construit une AccessRequest et délègue au PDP. |
RequirePermission(resourceType, action) |
décorateur | Déclare l'accès requis d'une route. |
PublicRoute() |
décorateur | Marque une route publique (opt-out volontaire). |
CurrentPrincipal() |
décorateur de paramètre | Injecte le Principal (posé sur req.user). |
KENGELA_PDP |
jeton (symbol) | Point d'injection de l'implémentation PolicyDecisionPoint. |
StepUpRequiredException |
exception | Levée quand le PDP renvoie step_up. |
Le guard s'installe en APP_GUARD (global). Le PDP est fourni sous le jeton KENGELA_PDP :
l'application choisit son implémentation (ici LayeredDecisionPoint).
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { KengelaAuthzGuard, KENGELA_PDP } from '@kengela/nestjs';
import { LayeredDecisionPoint } from '@kengela/authz-core';
import { CelExpressionEngine } from '@kengela/adapter-expr-cel';
import type { PolicyDecisionPoint } from '@kengela/contracts';
@Module({
providers: [
{
provide: KENGELA_PDP,
useFactory: (grants, relations, policies): PolicyDecisionPoint =>
new LayeredDecisionPoint({ grants, relations, policies, expr: new CelExpressionEngine() }),
inject: [/* vos providers AuthorizationRepository, RelationResolver, PolicyStore */],
},
{ provide: APP_GUARD, useClass: KengelaAuthzGuard },
],
})
export class AuthzModule {}Le Principal doit être posé sur req.user en amont du guard (middleware/guard
d'authentification, ex. via BetterAuthIdentity ou IdentityPort, voir
03-authentication.md).
@RequirePermission(resourceType, action) déclare la permission `${resourceType}.${action}`.
@CurrentPrincipal() injecte le Principal courant :
import { Controller, Get, Post } from '@nestjs/common';
import { RequirePermission, PublicRoute, CurrentPrincipal } from '@kengela/nestjs';
import type { Principal } from '@kengela/contracts';
@Controller('orders')
export class OrdersController {
@Get()
@RequirePermission('data.orders', 'read')
list(@CurrentPrincipal() principal: Principal) {
// atteint seulement si le PDP a répondu allow
}
@Post('refund')
@RequirePermission('data.orders', 'refund')
refund() {
// si une policy renvoie step_up → StepUpRequiredException (voir plus bas)
}
@Get('health')
@PublicRoute()
health() {
return { ok: true };
}
}Deux garanties de sécurité, prouvées par test :
-
Deny-by-default. Une route sans
@RequirePermissionni@PublicRouteest refusée (ForbiddenException('route_not_annotated')). On n'expose jamais une route par oubli d'annotation. UnPrincipalabsent sur une route protégée →UnauthorizedException('no_principal')(401). -
Précédence handler > classe (fail-closed). L'annotation du handler prime toujours sur celle de la classe. Un
@PublicRoute()posé sur le contrôleur ne peut pas neutraliser un@RequirePermissionposé sur un handler. L'ordre exact :Priorité Annotation Effet 1 handler @RequirePermissionon évalue (même si la classe est publique) 2 handler @PublicRoutepublic 3 classe @RequirePermissionon évalue 4 classe @PublicRoutepublic 5 rien deny (route non annotée)
Ce durcissement corrige un fail-open classique où un @PublicRoute de classe rendait publiques
toutes les routes, y compris un handler sensible.
Le guard construit une AccessRequest (ressource au niveau type + tenant du principal), appelle
pdp.check(), et mappe l'effet :
decision.effect |
Résultat HTTP |
|---|---|
allow |
la requête passe |
deny |
ForbiddenException(decision.reason) → 403 |
step_up |
StepUpRequiredException(obligations, reason) → 403 enrichi |
// Corps de la StepUpRequiredException (403) :
{
statusCode: 403,
error: 'step_up_required',
reason: 'refund_needs_passkey',
obligations: [{ type: 'require_passkey' }],
}Côté client, step_up_required déclenche un défi (relancer MFA/passkey), pas un échec définitif.
Portée du guard. Le guard couvre le RBAC + les conditions de contexte (
principal.ctx: risque/géo/mfa = conditional access). Les conditions ABAC sur les attributs d'une ressource précise (ex. « même agence ») se vérifient au niveau service, en appelant directement le PDP avec la ressource chargée :
@Injectable()
export class OrdersService {
constructor(@Inject(KENGELA_PDP) private readonly pdp: PolicyDecisionPoint) {}
async read(principal: Principal, order: Order) {
const decision = await this.pdp.check({
principal,
action: 'read',
resource: {
type: 'data.orders',
id: order.id,
tenantId: principal.tenantId,
attributes: { agencyId: order.agencyId, ownerId: order.ownerId },
},
});
if (decision.effect !== 'allow') throw new ForbiddenException(decision.reason);
return order;
}
}Le guard filtre grossièrement (peut-on lire des commandes ?) ; le service tranche finement (peut-on lire cette commande ?).
@kengela/scim-server est framework-agnostique (handlers purs). Un contrôleur NestJS se contente de
résoudre le tenant, parser le corps, appeler le handler et renvoyer status/body. Voir
04-identity-federation.md.
🇬🇧 English
- Home
- Getting started
- Architecture
- Authorization
- Authentication
- Identity federation
- Compliance & PII
- NestJS integration
- Developing an adapter
- Security
Recipes
- NestJS + native + Prisma
- better-auth
- SCIM / Entra
- LDAP / AD
- RBAC / ABAC
- PII / GDPR
- Combo: better-auth + PII
- Combo: SCIM/Entra + authz
- Combo: full stack
🇫🇷 Français
- Accueil
- Prise en main
- Architecture
- Autorisation
- Authentification
- Fédération d'identité
- Conformité & PII
- Intégration NestJS
- Développer un adapter
- Sécurité
Recettes