Notez l'article 1 Star2 Stars3 Stars4 Stars5 Stars (Pas encore de vote)
Loading...

Système de permissions personnalisé avec spring-boot (ACL)

Je vous invite, après lecture, à me laisser un commentaire ou à noter cet article afin de m'aider à m'améliorer (ou me corriger si besoin). Je vous en serais très reconnaissant. De même n'hésitez pas à intervenir si vous avez des questions. Bonne Lecture !

Une chose assez récurrente dans une application est d’avoir besoin de définir ce qu’un utilisateur peut faire ou ne peut pas faire. Nativement Spring-security permet de filtrer les autorisations avec les fonctions « hasRole() ». Cependant ne filtrer que sur les rôles est assez limitant car certaines application peuvent nécessiter une gestion de droit plus complexe et plus granuleuse.

Il existe pour cela avec Spring le module Spring-ACL (Access Control List) qui fonctionne avec Spring-security.

Pour ma part je trouve que Spring-ACL est très complexe à mettre en place, difficilement lisible et offre une complexité qui n’est pas forcément nécessaire pour une petite ou moyenne application. En outre il manque de souplesse pour sa mise en place sur un modèle de données spécifique ou déjà existant.

Je propose donc ici une implémentation de filtres pour @preAuthorize et @postAuthorize de Srpring-security qui permettrons de faire du filtrage façon ACL.

Si vous avez déjà implémenté spring-security il est fort probable que vous ayez une table Role (ou qui s’y apparente) dans votre modèle de données.

L’implémentation ici proposée nécessitera d’implémenter une nouvelle table (et une seule) dans votre modèle de données. La table des privilèges qui référencera les différents droits par type d’action, type d’objet visé et par rôle.

Cette solution n’est sans doute pas la meilleure mais fonctionne très bien et comporte l’avantage d’être relativement simple à mettre en place puisqu’elle ne nécessite que très peu de modifications du modèle de données.

 

Prérequis :

  • Avoir implémenté Spring-security pour disposer du UserPrincipal et des filtres @preAuthorize ou @postAuthorize
  • S’assurer que le UserPrincipal contiens le rôle de l’utilisateur

 

Mise en situation

Admettons une application de gestion de contrats qui dispose de deux rôles.

  • MANAGER : le directeur ou chef d’equipe qui peut saisir des contrats, des collaborateur et les modifier sans contrainte
  • COLLABORATEUR : qui ne peut voir et modifier que lui même ou eventuellement certains autres utilisateurs sous certaines conditions. Il ne peut également agir que sur les contrat sur lesquels il a été affecté (ce qui présuppose qu’il existe une table de mapping des User par Contract)

Arrêtons nous sur la capacité d’agir sur les utilisateurs :

On voit ici que la gestion des permissions peut s’avérer complexe car aussi bien les managers que les collaborateurs ont des droits sur les utilisateurs mais, pour les uns, sur tous les utilisateurs, et pour les autres, sur eux-même ou, en tout cas, sous certaines conditions que vous définirez.

Un simple hasRole que permet Spring-security n’est donc pas envisageable car :

  • si l’on met un @preAuthorize(« hasRole(ROLE_MANAGER) ») sur un update du User alors le rôle COLLABORATEUR ne pourras pas le modifier.
  • si on met un @preAuthorize(« hasRole(ROLE_MANAGER) or hasRole(ROLE_COLLABORATEUR) »), les deux rôles pourront modifier tout les utilisateurs. Ce n’est pas ce que nous voulons ici. Nous préférerions qu’effectivement le MANAGER puisse modifier tout les collaborateurs et que le COLLABORATEUR ne puisse modifier que lui-même ou peut-être d’autres mais sous certaines conditions.

 

Création des rôles

Tout d’abord définissons les rôles : (Votre modèle de Role peut être complètement différent, nous aurons juste besoin de son ID et de référencer les privilèges)

package com.app.authorization.model;

import javax.persistence.*;
import java.util.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "role")
@Getter @Setter @NoArgsConstructor
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 20) 
    private String name;

    @Column(length = 6)
    private String longName;

    @OneToMany(mappedBy="role")
    private Set<Privilege> privileges = new HashSet<Privilege>();

}
Role

 

Création des privilèges

Pour les privilèges représentons nous tout d’abord ce qu’ils sont.

Un privilege est en definitive une action autorisée sur une entité exemple : lire un contrat.

Néanmoins certains couple action/entité comporterons des règles métier afin de déterminer si pour un rôle précis, « lire contrat » est autorisé pour tout les contrats ou seulement un type de contrat ou les contrats affectés etc…(le nombre de règles n’ont de limites que le périmètre sur lequel vous travaillez…)

il faut donc un indicateur pour savoir si ce couple action/entité (ce privilège) comportera des règles (sera contraint ou non).

On ajoutera donc a notre modèle un indicateur boolean et l’on rattachera ce privilège à un rôle.


package com.app.authorization.model;

import javax.persistence.*;
import java.util.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
@Table(name = "privilege")
@Getter @Setter @NoArgsConstructor
public class Privilege {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 20) 
    private String action;

    @Column(length = 40)
    private String entity;

    @Column
    private Boolean constrained;

    @ManyToOne
    @JoinColumn(name="role_id")
    @JsonIgnore
    private Role role;

    public Boolean isConstrained() {
        return constrained;
    }

    public void setIsConstrained(Boolean isConstrained){
        constrained = isConstrained;
    }

}
Privilege

 

On pourra finalement référencer dans la table privilège une série de capacités pour chaque rôle qui ressemblera à ceci.

id action entity constrained role_id
1 list User 1 2
2 update User 1 2
3 update User 0 1
4 affect Contract 0 1
5

Ici on voit par exemple que :

  • le role_id 2 (COLLABORATEUR) peut modifier les utilisateurs mais avec une ou des conditions (constrained = 1) (conditions qui seront définies dans les règles métier un peu plus tard..)
  • le role_id 1 (MANAGER) peut modifier les utilisateurs sans condition (constrained = 0)

 

Il va ensuite falloir permettre à nos filtre d’aller chercher en base un privilège, en fonction de l’action demandée, de l’objet visé et du rôle de l’utilisateur connecté via le Repository de JPA.

package com.app.authorization.repository;

import com.app.authorization.model.Privilege;
import com.app.authorization.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PrivilegeRepository extends JpaRepository<Privilege, Long> {

    Privilege findPrivilegeByActionAndEntityAndRole(String action, String entity, Role role);
}
PrivilegeRepository

 

Service de vérification et règles métier

II ne nous reste plus qu’a implémenter le service de vérification des privilèges qui contiendra entre-autre les règles métier pour les privilèges conditionnés.

package com.app.authorization.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.security.core.context.SecurityContextHolder;

import com.app.authorization.model.*;
import com.app.authorization.repository.*;
import com.app.security.UserPrincipal;


import java.util.*;
import java.lang.*;

/**
 * Service de vérification des privileges utilisateurs.
 */
@Service
public class AuthorizationSE {

    @Autowired
    private UserRepository userRepo;

    @Autowired
    private PrivilegeRepository privilegeRepo;

    /**
     * Vérifie l'autorisation pour les privileges qui ne sont pas sensé contenir de contraintes sur l'objet visé
     * @param action le type d'action demandée
     * @param entity l'objet visé
     * @return
     */
    public boolean can(String action, String entity) {
        UserPrincipal currentUser = (UserPrincipal)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        //recuperation du privilege par action et par entité : logiquement il n'en existe qu'un par role avec cette action et cet objet
        Privilege privilege = privilegeRepo.findByActionAndEntityAndRole(action, entity, currentUser.getRole());
        //si privileges existe et qu'il n'attend pas de vérification de contrainte
        return (null != privilege && !privilege.isConstrained());

    }

    /**
     * Vérifie l'autorisations pour les privileges qui comportent des contraintes sur l'objet visé
     * @param action le type d'action demandée
     * @param entity l'objet visé
     * @param entityId l'id de l'objet visé
     * @return Vrai ou Faux
     */
    public boolean can(String action, String entity, Long entityId) {

        UserPrincipal currentUser = (UserPrincipal)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        boolean authorized = false;
        //recuperation du privilege par action et par entité : Il ne peut en exister qu'un par role
        Privilege privilege = privilegeRepo.findByActionAndEntityAndRole(action, entity, currentUser.getRole());

        if (null == privilege) {
            return authorized;
        }

        //implémentation des logiques métier de vérification des contraintes
        switch (entity){
            //Vérification si l'utilisateur est affecté au contrat via la table d'affectation UserContract
            case "Contract":
                Optional<User> user = userRepo.findById(currentUser.getId());
                //Récuperation des contrats sur lesquel est affecté l'utilisateur
                List<UserContracts> userContracts = user.get().getUserContracts();
                for (UserContract userContract : userContracts) {
                    if (userContract.getContract().getId().equals(entityId)) {
                        authorized = true;
                        break;
                    }
                }
                break;
            //Vérification si l'utilisateur visé par la requete est le même que l'utilisateur actuellement authentifié
            case "User":
                if (currentUser.getId().equals(entityId)) {
                    authorized = true;
                }
                break;
            default :
                authorized = false;
                break;
        }

        return authorized;

    }

}
AuthorizationSE

 

Appel du service

On peut désormais appeler ce service dans les filtre @preAuthorize et @postAuthorize par exemple :

  • vérifier si l’utilisateur courant PEU METTRE A JOUR TOUT LES UTILISATEURS

@preAuthorize(« @abilityService.can(‘update’, ‘User’) »)

ou

  • vérifier si l’utilisateur courant PEU METTRE A JOUR CET UTILISATEUR EN PARTICULIER

@preAuthorize(« @abilityService.can(‘update’, ‘User’, #UserId) »)

(où le #UserId est l’id du User envoyé dans la requête. la fonction can() vérifiera si l’id demandé correspond à celui de l’utilisateur courant)

 

On pourra tout à fait combiner les deux à savoir :

@preAuthorize(« @abilityService.can(‘update’, ‘User’) or @abilityService.can(‘update’, ‘User’, #UserId) »)

ce qui reviens à dire : « vérifier si l’utilisateur courant dispose des droits de modification sur tout les utilisateurs OU sur celui-ci en particulier »

les règles de gestion à ce niveau varieront donc d’un projet à l’autre. ici nous avons pris la comparaison très simple entre l’id de l’utilisateur courant et celui de l’objet visé, ce qui empêche l’utilisateur de modifier un autre utilisateur que lui même.

 

Ci-dessous : mieux que des mots :

package com.app.controller;

import org.springframework.web.bind.annotation.*;
import com.app.authorization.service.AuthorizationSE;

@RestController
public class UserController {

    /**
    * Met à jour un utilisateur en fonction des permissions
    */
    @preAuthorize("@authorizationSE.can('update', 'User') or @authorizationSE.can('update', 'User', #userId)")
    @PutMapping("/user/update/{userId}")
    public User updateUser(@PathVariable(value = "userId") Long userId){
        //code here...
    }
}
UserController

 

Conclusion

L’exemple pris ici pour illustrer mes propos est très simpliste mais ce système peut permettre de créer une gestion d’habilitation pour une application plusieurs niveaux de droits avec des permissions très entremêlées, et ce, de manière relativement simple et lisible.

J’espère que cet article à été clair et utile pour vous. Merci de noter cet article, et le must serait de me laisser un petit commentaire ! C’est toujours sympa de savoir ce qu’il y a de bien ou moins bien, et s’il y a une question ou une remarque discutons-en 😉

Notez l'article 1 Star2 Stars3 Stars4 Stars5 Stars (Pas encore de vote)
Loading...

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Show Buttons
Hide Buttons