Introduction
Strapi offre une souplesse remarquable, ce qui signifie qu’il existe généralement plusieurs façons de mettre en place une fonctionnalité donnée. Nous préférons toujours choisir l’approche la plus judicieuse en fonction de la fonctionnalité à implémenter, en veillant à minimiser l’impact sur la modification du fonctionnement standard des APIs de Strapi. Dans le contexte de notre sujet d’aujourd’hui, qui concerne la restriction de l’accès d’une entité spécifique à son unique propriétaire, différentes approches sont envisageables, notamment l’utilisation d’une policy, d’un middleware, ou encore d’un controller.
Considérons une application de gestion des tâches qui prend en charge la gestion d’utilisateurs et de leurs tâches associées. Chaque tâche est liée à un utilisateur spécifique, identifié par son ID.
Dans ce scénario, notre objectif est de garantir que seule la personne propriétaire d’une tâche a la possibilité de consulter, modifier et supprimer celle-ci via l’API. Cette approche vise à instaurer un contrôle d’accès strict tout en assurant la protection des données personnelles des utilisateurs.
Stratégie
Initialement, lors de la création d’un type de contenu, Strapi génère cinq opérations par défaut : Create, Find, FindOne, Update et Delete. Notre objectif est de présenter une stratégie de sécurisation de nos endpoints, alignée de manière cohérente avec les concepts fondamentaux de policy, middleware et controller.
Pour les opérations findOne, update et delete qui ciblent des enregistrements spécifiques, nous privilégions l’utilisation de policies. En effet, leur objectif premier est de gérer le contrôle d’accès, elle retourne true par accorder la permission ou false pour refuser l’accès.
Quant à l’opération create, nous favorisons l’utilisation de middleware. Lorsqu’un utilisateur tente de créer un enregistrement, nous devons modifier la requête pour injecter l’ID de l’utilisateur connecté en tant que propriétaire de cette tâche. Ce cas d’utilisation correspond parfaitement à la définition d’un middleware, permettant d’effectuer des actions telles que la modification de la requête et la validation des données.
Enfin, pour l’opération find, nous optons également pour l’utilisation de middleware. Dans ce cas également, nous allons injecter des filtres au niveau de la requête afin de retourner uniquement les enregistrements dont l’utilisateur connecté est le propriétaire.
Création du content-type Task
Pour le content-type Task
nous aurons les champs ci-dessous :
- Field:
name
, Type:string
outext short
- Field:
isDone
, Type: Boolean - Relation:
User
, Type:Has many
Exécutez la commande et suivez les instructions ci-dessous pour créer l’entité “Task”. Comme le type de champ relation n’est pas disponible dans la console, il sera ajouté ultérieurement via l’interface.
yarn strapi generate content-type #Répondez de la manière suivante : Content type display name Task Content type singular name task Content type plural name tasks Please choose the model type Collection Type Use draft and publish? No Do you want to add attributes? Yes Name of attribute name What type of attribute string Do you want to add another attribute? Yes Name of attribute isDone What type of attribute boolean Do you want to add another attribute? No Where do you want to add this model? Add model to new API Name of the new API? task Bootstrap API related files? Yes ✔ ++ /api/task/content-types/task/schema.json ✔ +- /api/task/content-types/task/schema.json ✔ ++ /api/task/controllers/task.ts ✔ ++ /api/task/services/task.ts ✔ ++ /api/task/routes/task.ts
Se rendre sur le menu “Content-type Builder” > “Task” afin d’ajouter un nouveau champ de type relation. Veuillez prendre soin de sélectionner le modèle “User” provenant de la section “users-permissions” plutôt que celui issu de la section “admin”. Car Strapi gère de manière distincte les utilisateurs ayant accès au back-office (admin) et les utilisateurs finaux de l’application (users-permissions).
Remplissage des données
Commencez par créer un compte utilisateur en insérant les informations nécessaires via le panneau d’administration Strapi. Il s’agit de la première étape pour permettre l’association ultérieure de Task à cet utilisateur.
Ajoutons désormais une entré dans Task
en l’associant à l’utilisateur que nous avons créé précédemment.
Répétez ces deux actions afin de créer un nouvel utilisateur et une nouvelle tâche qui lui est associée. Ceci nous permettra de vérifier que notre utilisateur n’a pas accès aux tâches des autres.
Configurer les permissions
Pour configurer les autorisations d’accès pour l’entité “Task”, suivez ces étapes de manière détaillée :
- Accédez à la section “Settings” > “Roles”.
- Sélectionnez le rôle “Authenticated”.
- Dans la section “User-Permissions > Task”, assurez-vous que les autorisations suivantes sont correctement activées : “update”, “findOne”, “delete”, “find” et “create”.
- Ensuite, sélectionnez “Users-permissions”.
- Dans la section “User”, vérifiez que les autorisations “find” et “findOne” sont correctement activées.
Initiallement, ces autorisations seront ouvertes à tous les utilisateurs connectés. Cependant, nous allons ultérieurement les restreindre en mettant en place une politique de sécurité.
Policy pour les opérations findOne, update et delete
Nous allons élaborer une policy qui sera invoquée lors de la récupération, la modification ou la suppression d’une entrée au sein de notre content-type “Task”. Cette policy aura pour rôle de vérifier que l’utilisateur actuellement connecté détient bel et bien les informations “Task” qu’il souhaite manipuler :
yarn strapi generate policy is-owner #Répondez de la manière suivante : Where do you want to add this policy? Add policy to an existing API Which API is this for? task ✔ ++ /api/task/policies/is-owner.ts
Dans le fichier généré (src/api/task/policies/is-owner.ts
), ajoutez le code suivant :
export default async (policyContext, config, { strapi }) => { const user = policyContext.state.user; const entryId = policyContext.params.id; if (!entryId) { // Si l'identifiant de l'entrée est manquant, la politique échoue return false; } try { const entry = await strapi.entityService.findOne( "api::task.task", entryId, { populate: ["users_permissions_user"] } ); // Vérifier si l'utilisateur actuel est le propriétaire de l'entrée return user.id === entry.users_permissions_user.id; } catch (error) { // Gérer les erreurs lors de la recherche de l'entrée return false; } };
Middleware pour l’opération find
Nous allons concevoir un middleware qui modifiera la requête en injectant un filtre sur le propriétaire des tâches. Ainsi, chaque utilisateur connecté ne pourra voir que ses propres tâches.
yarn strapi generate middleware filter-owner #Répondez de la manière suivante : Where do you want to add this middleware? Add middleware to an existing API Which API is this for? task ✔ ++ /api/task/middlewares/filter-owner.ts
Dans le fichier généré (src/api/middlewares/policies/filter-owner.ts
), ajoutez le code suivant :
import { Strapi } from '@strapi/strapi'; export default (config, { strapi }: { strapi: Strapi }) => { // Add your own logic here. return async (ctx, next) => { const user = ctx.state.user; ctx.query = { ...ctx.query, filters: { users_permissions_user: { id: { $eq: user.id } } }, }; await next(); }; };
On pourrait penser que ce filtre aurait pu être suffisant pour gérer également nos opérations findOne, update et delete, mais cela aurait été insuffisant, car les filtres ne sont pris en compte que lors de l’opération find.
Middleware pour l’opération create
Créons un middleware qui assure que l’ID de l’utilisateur connecté est automatiquement associé lorsqu’il crée une nouvelle entrée dans notre entité “task”.
yarn strapi generate middleware inject-owner #Répondez de la manière suivante : Where do you want to add this middleware? Add middleware to an existing API Which API is this for? task ✔ ++ /api/task/middlewares/inject-owner.ts
Dans le fichier généré (src/api/middlewares/policies/inject-owner.ts
), ajoutez le code suivant :
import { Strapi } from '@strapi/strapi'; export default (config, { strapi }: { strapi: Strapi }) => { return async (ctx, next) => { const user = ctx.state.user; // Affecter l'ID de l'utilisateur comme propriétaire dans la requête ctx.request.body.data = { ...ctx.request.body.data, users_permissions_user: user.id, }; await next(); }; };
Configuration des middleware et policies
Pour attacher nos middlewares et policies à notre route, nous aurons besoin de leur nom système. Pour l’obtenir, veuillez exécuter les commande ci-dessous :
# Liste les systèmes des middlewares yarn strapi middlewares:list # Liste les systèmes des policies yarn strapi policies:list
Ouvrez le fichier
et configurez nos middlewares et policies pour être appelé lors de l’invocation de notre api :src/api/task/routes/
task.ts
import { factories } from '@strapi/strapi'; export default factories.createCoreRouter('api::task.task', { config: { create: { middlewares: ["api::task.inject-owner"], }, find: { middlewares: ["api::task.filter-owner"], }, findOne: { policies: ["api::task.is-owner"], }, update: { policies: ["api::task.is-owner"], }, delete: { policies: ["api::task.is-owner"], }, }, });
Conclusion
En conclusion, la flexibilité offerte par Strapi se révèle être un atout majeur dans la mise en place de fonctionnalités spécifiques tout en respectant les normes et les bonnes pratiques. Dans le cadre de notre exploration sur la restriction d’accès à une entité spécifique à son unique propriétaire, nous avons examiné différentes approches telles que l’utilisation de policies, de middlewares.
En adoptant une approche réfléchie, nous avons défini une stratégie cohérente pour sécuriser nos endpoints, alignée avec les concepts fondamentaux de Strapi. Les opérations findOne, update et delete, ciblant des enregistrements spécifiques, ont été traitées efficacement à l’aide de policies, offrant un contrôle d’accès précis. Pour l’opération create, où l’ajout de données spécifiques est nécessaire, l’utilisation d’un middleware s’est avérée pertinente. De même, l’opération find a été sécurisée en utilisant un middleware pour filtrer les enregistrements selon le propriétaire.
Architecte logiciel, Développeur d'application diplomé d'ETNA, la filière d'alternance d'Epitech, j'ai acquis une expertise solide dans le développement d'applications, travaillant sur des projets complexes et techniquement diversifiés. Mon expérience englobe l'utilisation de divers frameworks et langages, notamment Symfony, Api Platform, Drupal, Zend, React Native, Angular, Vue.js, Shell, Pro*C...