Autorisation
L’autorisation fait référence au processus qui détermine ce qu’un utilisateur est capable de faire. Par exemple, un utilisateur administratif est autorisé à créer, modifier et supprimer des publications. Un utilisateur non administratif est seulement autorisé à lire les publications.
L’autorisation est orthogonale et indépendante de l’authentification. Cependant, l’autorisation nécessite un mécanisme d’authentification.
Il existe de nombreuses approches et stratégies différentes pour gérer l’autorisation. L’approche adoptée pour un projet particulier dépend de ses exigences spécifiques. Ce chapitre présente quelques approches d’autorisation qui peuvent être adaptées à différents besoins.
Implémentation de base du RBAC
Le contrôle d’accès basé sur les rôles (RBAC) est un mécanisme de contrôle d’accès neutre défini autour des rôles et des privilèges. Dans cette section, nous allons démontrer comment mettre en œuvre un mécanisme RBAC très basique en utilisant les gardiens de Nest.
Tout d’abord, créons un énumérateur Role
représentant les rôles du système :
export enum Role { User = 'user', Admin = 'admin',}
Avec cela en place, nous pouvons créer un décorateur @Roles()
. Ce décorateur permet de spécifier quels rôles sont requis pour accéder à des ressources spécifiques.
import { SetMetadata } from '@nestjs/common';import { Role } from '../enums/role.enum';
export const ROLES_KEY = 'roles';export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
Maintenant que nous avons un décorateur @Roles()
, nous pouvons l’utiliser pour décorer n’importe quel gestionnaire de route.
@Post()@Roles(Role.Admin)create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto);}
Enfin, nous créons une classe RolesGuard
qui comparera les rôles attribués à l’utilisateur actuel avec les rôles nécessaires pour la route en cours de traitement. Pour accéder aux rôles de la route (métadonnées personnalisées), nous utiliserons la classe d’assistance Reflector
, qui est fournie par le framework et exposée à partir du package @nestjs/core
.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';
@Injectable()export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [ context.getHandler(), context.getClass(), ]); if (!requiredRoles) { return true; } const { user } = context.switchToHttp().getRequest(); return requiredRoles.some((role) => user.roles?.includes(role)); }}
Dans cet exemple, nous avons supposé que request.user
contient l’instance utilisateur et les rôles autorisés (sous la propriété roles
). Dans votre application, vous allez probablement établir cette association dans votre garde d’authentification personnalisée - voir le chapitre authentification pour plus de détails.
Pour que cet exemple fonctionne, votre classe User
doit ressembler à ceci :
class User { // ...autres propriétés roles: Role[];}
Enfin, veillez à enregistrer le RolesGuard
, par exemple au niveau du contrôleur ou globalement :
providers: [ { provide: APP_GUARD, useClass: RolesGuard, },],
Lorsqu’un utilisateur avec des privilèges insuffisants demande un point de terminaison, Nest retourne automatiquement la réponse suivante :
{ "statusCode": 403, "message": "Ressource interdite", "error": "Interdit"}
Autorisation basée sur les revendications
Lorsque qu’une identité est créée, elle peut être assignée une ou plusieurs revendications émises par une partie de confiance. Une revendication est une paire nom-valeur qui représente ce que le sujet peut faire, et non ce que le sujet est.
Pour implémenter une autorisation basée sur les revendications dans Nest, vous pouvez suivre les mêmes étapes que celles décrites ci-dessus dans la section RBAC avec une différence significative : au lieu de vérifier des rôles spécifiques, vous devez comparer les permissions. Chaque utilisateur aurait un ensemble de permissions assignées. De même, chaque ressource/point de terminaison définirait quelles permissions sont requises (par exemple, via un décorateur @RequirePermissions()
) pour y accéder.
@Post()@RequirePermissions(Permission.CREATE_CAT)create(@Body() createCatDto: CreateCatDto) { this.catsService.create(createCatDto);}
Intégration de CASL
CACL est une bibliothèque d’autorisation isomorphe qui restreint les ressources auxquelles un client donné est autorisé à accéder. Elle est conçue pour être adoptée progressivement et peut facilement évoluer entre une autorisation basée sur des revendications simples et une autorisation complète basée sur des sujets et des attributs.
Pour commencer, installez d’abord le package @casl/ability
:
$ npm i @casl/ability
Une fois l’installation terminée, afin d’illustrer la mécanique de CASL, nous allons définir deux classes d’entités : User
et Article
.
class User { id: number; isAdmin: boolean;}
La classe User
se compose de deux propriétés : id
, qui est un identifiant unique pour l’utilisateur, et isAdmin
, qui indique si l’utilisateur a des privilèges d’administrateur.
class Article { id: number; isPublished: boolean; authorId: number;}
La classe Article
a trois propriétés : respectivement id
, isPublished
et authorId
. id
est un identifiant unique pour l’article, isPublished
indique si un article a déjà été publié ou non, et authorId
, qui est l’ID de l’utilisateur qui a écrit l’article.
Examinons maintenant et affinons nos exigences pour cet exemple :
- Les administrateurs peuvent gérer (créer/lire/mettre à jour/supprimer) toutes les entités.
- Les utilisateurs ont un accès en lecture seule à tout.
- Les utilisateurs peuvent mettre à jour leurs articles (
article.authorId === userId
). - Les articles qui sont déjà publiés ne peuvent pas être supprimés (
article.isPublished === true
).
Avec cela en tête, nous pouvons commencer à créer une énumération Action
représentant toutes les actions possibles que les utilisateurs peuvent effectuer avec les entités.
export enum Action { Manage = 'manage', Create = 'create', Read = 'read', Update = 'update', Delete = 'delete',}
Remarque : manage
est un mot-clé spécial dans CASL qui représente “toute” action.
Pour encapsuler la bibliothèque CASL, générons le CaslModule
et le CaslAbilityFactory
maintenant.
$ nest g module casl$ nest g class casl/casl-ability.factory
Avec cela en place, nous pouvons définir la méthode createForUser()
sur la classe CaslAbilityFactory
. Cette méthode créera l’objet Ability
pour un utilisateur donné.
type Subjects = InferSubjects<typeof Article | typeof User> | 'all';
export type AppAbility = Ability<[Action, Subjects]>;
@Injectable()export class CaslAbilityFactory { createForUser(user: User) { const { can, cannot, build } = new AbilityBuilder<Ability<[Action, Subjects]>>(Ability as AbilityClass<AppAbility>);
if (user.isAdmin) { can(Action.Manage, 'all'); // accès en lecture-écriture à tout } else { can(Action.Read, 'all'); // accès en lecture seule à tout }
can(Action.Update, Article, { authorId: user.id }); cannot(Action.Delete, Article, { isPublished: true });
return build({ // Lisez https://casl.js.org/v6/en/guide/subject-type-detection#use-classes-as-subject-types pour plus de détails detectSubjectType: (item) => item.constructor as ExtractSubjectType<Subjects>, }); }}
Remarque : all
est un mot-clé spécial dans CASL qui représente “tout sujet”.
Dans l’exemple ci-dessus, nous avons créé l’instance Ability
en utilisant la classe AbilityBuilder
. Comme vous l’avez probablement deviné, can
et cannot
acceptent les mêmes arguments mais ont des significations différentes, can
permet d’effectuer une action sur le sujet spécifié et cannot
interdit. Les deux peuvent accepter jusqu’à quatre arguments. Pour en savoir plus sur ces fonctions, consultez la documentation officielle de CASL.
Enfin, assurez-vous d’ajouter CaslAbilityFactory
aux tableaux providers
et exports
dans la définition du module CaslModule
.
import { Module } from '@nestjs/common';import { CaslAbilityFactory } from './casl-ability.factory';
@Module({ providers: [CaslAbilityFactory], exports: [CaslAbilityFactory],})export class CaslModule {}
Avec cela en place, nous pouvons injecter CaslAbilityFactory
dans n’importe quelle classe en utilisant l’injection de constructeur standard tant que le CaslModule
est importé dans le contexte hôte.
constructor(private caslAbilityFactory: CaslAbilityFactory) {}
Ensuite, utilisez-le dans une classe comme suit :
const ability = this.caslAbilityFactory.createForUser(user);if (ability.can(Action.Read, 'all')) { // "l'utilisateur" a accès en lecture à tout}
Par exemple, disons que nous avons un utilisateur qui n’est pas un administrateur. Dans ce cas, l’utilisateur devrait pouvoir lire des articles, mais créer de nouveaux ou supprimer les articles existants devrait être interdit.
const user = new User();user.isAdmin = false;
const ability = this.caslAbilityFactory.createForUser(user);ability.can(Action.Read, Article); // trueability.can(Action.Delete, Article); // falseability.can(Action.Create, Article); // false
Également, comme nous l’avons spécifié dans nos exigences, l’utilisateur doit pouvoir mettre à jour ses articles :
const user = new User();user.id = 1;
const article = new Article();article.authorId = user.id;
const ability = this.caslAbilityFactory.createForUser(user);ability.can(Action.Update, article); // true
article.authorId = 2;ability.can(Action.Update, article); // false
Comme vous pouvez le voir, l’instance Ability
nous permet de vérifier les permissions de manière très lisible. De même, AbilityBuilder
nous permet de définir des permissions (et de spécifier diverses conditions) de manière similaire. Pour trouver plus d’exemples, consultez la documentation officielle.
Avancé : Implémentation d’un PoliciesGuard
Dans cette section, nous allons démontrer comment construire une garde quelque peu plus sophistiquée, qui vérifie si un utilisateur satisfait des politiques d’autorisation spécifiques qui peuvent être configurées au niveau de la méthode (vous pouvez également l’étendre pour respecter les politiques configurées au niveau de la classe). Dans cet exemple, nous allons utiliser le package CASL uniquement à des fins d’illustration, mais l’utilisation de cette bibliothèque n’est pas requise. De plus, nous allons utiliser le fournisseur CaslAbilityFactory
que nous avons créé dans la section précédente.
Tout d’abord, clarifions les exigences. Le but est de fournir un mécanisme permettant de spécifier des vérifications de politiques par gestionnaire de route. Nous allons prendre en charge à la fois les objets et les fonctions (pour des vérifications plus simples et pour ceux qui préfèrent un code de style fonctionnel).
Commençons par définir des interfaces pour les gestionnaires de politiques :
import { AppAbility } from '../casl/casl-ability.factory';
interface IPolicyHandler { handle(ability: AppAbility): boolean;}
type PolicyHandlerCallback = (ability: AppAbility) => boolean;
export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;
Comme mentionné ci-dessus, nous avons prévu deux façons possibles de définir un gestionnaire de politique, un objet (instance d’une classe qui implémente l’interface IPolicyHandler
) et une fonction (qui respecte le type PolicyHandlerCallback
).
Avec cela en place, nous pouvons créer un décorateur @CheckPolicies()
. Ce décorateur permet de spécifier quelles politiques doivent être respectées pour accéder à des ressources spécifiques.
export const CHECK_POLICIES_KEY = 'check_policy';export const CheckPolicies = (...handlers: PolicyHandler[]) => SetMetadata(CHECK_POLICIES_KEY, handlers);
Maintenant, créons un PoliciesGuard
qui extraira et exécutera tous les gestionnaires de politiques liés à un gestionnaire de route.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';import { Reflector } from '@nestjs/core';import { CaslAbilityFactory } from './casl/casl-ability.factory';import { PolicyHandler } from './policy.handler';
@Injectable()export class PoliciesGuard implements CanActivate { constructor(private reflector: Reflector, private caslAbilityFactory: CaslAbilityFactory) {}
async canActivate(context: ExecutionContext): Promise<boolean> { const policyHandlers = this.reflector.get<PolicyHandler[]>(CHECK_POLICIES_KEY, context.getHandler()) || [];
const { user } = context.switchToHttp().getRequest(); const ability = this.caslAbilityFactory.createForUser(user);
return policyHandlers.every((handler) => this.execPolicyHandler(handler, ability)); }
private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) { if (typeof handler === 'function') { return handler(ability); } return handler.handle(ability); }}
Analysons cet exemple. policyHandlers
est un tableau de gestionnaires affectés à la méthode via le décorateur @CheckPolicies()
. Ensuite, nous utilisons la méthode create
de CaslAbilityFactory
, qui construit l’objet Ability
, nous permettant de vérifier si un utilisateur dispose de permissions suffisantes pour effectuer des actions spécifiques. Nous transmettons cet objet au gestionnaire de politique qui est soit une fonction, soit une instance d’une classe qui implémente IPolicyHandler
, exposant la méthode handle()
qui renvoie un booléen. Enfin, nous utilisons la méthode Array#every
pour nous assurer que chaque gestionnaire a renvoyé une valeur true
.
Enfin, pour tester cette garde, attachez-la à n’importe quel gestionnaire de route et enregistrez un gestionnaire de politique en ligne (approche fonctionnelle) comme suit :
@Get()@UseGuards(PoliciesGuard)@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))findAll() { return this.articlesService.findAll();}
Alternativement, nous pouvons définir une classe qui implémente l’interface IPolicyHandler
:
export class ReadArticlePolicyHandler implements IPolicyHandler { handle(ability: AppAbility) { return ability.can(Action.Read, Article); }}
Et l’utiliser comme suit :
@Get()@UseGuards(PoliciesGuard)@CheckPolicies(new ReadArticlePolicyHandler())findAll() { return this.articlesService.findAll();}
Remarque : Comme nous devons instancier le gestionnaire de politique en place à l’aide du mot-clé new
, la classe ReadArticlePolicyHandler
ne peut pas utiliser l’injection de dépendance. Cela peut être résolu avec la méthode ModuleRef#get
(lisez-en plus ici). En gros, au lieu d’enregistrer des fonctions et des instances via le décorateur @CheckPolicies()
, vous devez permettre de passer un Type<IPolicyHandler>
. Ensuite, dans votre garde, vous pourriez récupérer une instance en utilisant une référence de type : moduleRef.get(YOUR_HANDLER_TYPE)
ou même l’instancier dynamiquement en utilisant la méthode ModuleRef#create
.