Contactez-nous

Gestion des permissions fines (Access Control Lists - ACL)

Explorez la gestion des permissions au niveau de l'instance d'objet avec Spring Security ACL. Apprenez à configurer et utiliser les listes de contrôle d'accès pour une sécurité granulaire.

Au-delà des rôles : le besoin de permissions fines

La gestion des autorisations basée sur les rôles (RBAC - Role-Based Access Control), bien qu'efficace pour de nombreux scénarios, atteint ses limites lorsque la décision d'accès dépend non seulement de qui fait la demande, mais aussi de l'objet spécifique auquel on tente d'accéder. Par exemple, un utilisateur peut avoir le rôle 'EDITEUR_DOCUMENT', mais ne devrait pouvoir éditer que les documents dont il est propriétaire ou sur lesquels on lui a explicitement donné la permission d'écriture.

Dans de tels cas, il faut un mécanisme de contrôle d'accès plus granulaire, opérant au niveau de chaque instance d'objet métier. C'est là qu'interviennent les Listes de Contrôle d'Accès (ACL - Access Control Lists).

Spring Security fournit un module dédié, Spring Security ACL, pour implémenter ce type de sécurité au niveau du domaine. Il permet d'associer des permissions spécifiques (lecture, écriture, administration, etc.) à des paires utilisateur/objet ou groupe/objet.

Les concepts fondamentaux des ACL Spring Security

Le module Spring Security ACL repose sur plusieurs concepts clés et une structure de base de données spécifique :

  • Object Identity (Identité d'Objet) : Représente une instance spécifique d'un objet de votre domaine métier (par exemple, un document précis, un message spécifique). Elle est généralement définie par le type de classe de l'objet et son identifiant unique (clé primaire).
  • SID (Security Identity - Identité de Sécurité) : Représente l'acteur (le 'qui') auquel une permission est accordée. Il peut s'agir d'un utilisateur individuel (Principal) ou d'une autorité/rôle (GrantedAuthority).
  • Permission : Définit le type d'accès accordé (par exemple, Lecture, Ecriture, Création, Suppression, Administration). Spring fournit des implémentations de base comme BasePermission.READ, BasePermission.WRITE, etc.
  • Access Control Entry (ACE - Entrée de Contrôle d'Accès) : C'est le coeur de l'ACL. Une ACE lie une Object Identity, un SID et une Permission. Elle spécifie qu'un SID donné a une Permission particulière sur une Object Identity spécifique. Une ACE peut être accordante (granting) ou refusante (non-granting, moins courant).
  • ACL (Access Control List - Liste de Contrôle d'Accès) : Une ACL est associée à une Object Identity unique et contient une liste ordonnée d'ACEs. Elle définit l'ensemble des permissions accordées sur cet objet spécifique. Les ACLs peuvent hériter de permissions d'une ACL parente (par exemple, un dossier parent).

Pour stocker ces informations, Spring Security ACL s'appuie typiquement sur quatre tables de base de données :

  • acl_sid : Stocke les identités de sécurité (principaux et autorités).
  • acl_class : Stocke les noms de classes des objets sécurisés.
  • acl_object_identity : Stocke les instances d'objets sécurisés, liant une classe (acl_class) et un identifiant d'objet, et potentiellement une ACL parente et le propriétaire (SID).
  • acl_entry : Stocke les ACEs, liant une identité d'objet (acl_object_identity), un SID (acl_sid), une permission (sous forme de masque binaire), et d'autres métadonnées (ordre, audit, type d'ACE).

Configuration du module Spring Security ACL

L'utilisation de Spring Security ACL nécessite une configuration spécifique car elle n'est pas incluse par défaut dans les starters habituels. Vous devez d'abord ajouter la dépendance :



    org.springframework.security
    spring-security-acl



implementation 'org.springframework.security:spring-security-acl'

Ensuite, vous devez configurer plusieurs beans essentiels dans une classe @Configuration :

  • DataSource : Une source de données pointant vers la base de données contenant les tables ACL.
  • LookupStrategy : Définit comment les ACLs et ACEs sont récupérées depuis la base de données. BasicLookupStrategy est l'implémentation standard.
  • AclAuthorizationStrategy : Détermine qui a le droit de modifier les ACLs elles-mêmes.
  • PermissionGrantingStrategy : Logique utilisée pour décider si une permission est accordée en consultant les ACEs.
  • AclCache : Un cache (souvent basé sur EhCache ou Caffeine) pour améliorer les performances en évitant des requêtes répétées à la base de données pour les ACLs.
  • MutableAclService : L'implémentation principale pour créer, modifier et supprimer des ACLs et des ACEs. JdbcMutableAclService est l'implémentation standard basée sur JDBC.

Voici un exemple de configuration de base :

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // Nécessaire pour @PreAuthorize/@PostAuthorize
public class AclConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public AclCache aclCache(PermissionGrantingStrategy pgs, AclAuthorizationStrategy aas) {
        // Utilisation de Caffeine pour le cache (nécessite la dépendance com.github.ben-manes.caffeine:caffeine)
        Cache caffeineCache = Caffeine.newBuilder().maximumSize(100).build();
        return new SpringCacheBasedAclCache(new CaffeineCache("aclCache", caffeineCache), pgs, aas);
    }

    @Bean
    public PermissionGrantingStrategy permissionGrantingStrategy() {
        // Stratégie standard pour vérifier les permissions
        return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
    }

    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() {
        // Stratégie simple : nécessite l'autorité ROLE_ADMIN pour modifier les ACLs
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }

    @Bean
    public LookupStrategy lookupStrategy(AclCache aclCache, AclAuthorizationStrategy aclAuthorizationStrategy,
                                         PermissionGrantingStrategy permissionGrantingStrategy) {
        return new BasicLookupStrategy(dataSource, aclCache, aclAuthorizationStrategy, permissionGrantingStrategy);
    }

    @Bean
    public MutableAclService mutableAclService(LookupStrategy lookupStrategy, AclCache aclCache) {
        JdbcMutableAclService aclService = new JdbcMutableAclService(dataSource, lookupStrategy, aclCache);
        // Optionnel : Configurer les identifiants des tables si différents des noms par défaut
        // aclService.setClassIdentityQuery("...");
        // aclService.setSidIdentityQuery("...");
        return aclService;
    }

    @Bean
    public PermissionEvaluator permissionEvaluator(MutableAclService aclService) {
        // Intègre l'évaluation des permissions ACL avec les expressions SpEL
        return new AclPermissionEvaluator(aclService);
    }
}

Utilisation pratique des ACLs

Une fois configuré, le système ACL peut être utilisé de deux manières principales : pour définir les permissions et pour les vérifier.

Définir les permissions : Cela se fait généralement après la création ou la mise à jour d'un objet métier. On utilise le bean MutableAclService pour créer ou récupérer l'ACL associée à l'objet, puis pour y insérer des ACEs.

@Autowired
private MutableAclService mutableAclService;

@Autowired
private UserDetailsService userDetailsService; // Pour obtenir le SID du principal

public void grantPermissions(MyDomainObject domainObject, String username, Permission permission) {
    UserDetails user = userDetailsService.loadUserByUsername(username);
    ObjectIdentity oi = new ObjectIdentityImpl(MyDomainObject.class, domainObject.getId());
    Sid sid = new PrincipalSid(user.getUsername());

    MutableAcl acl;
    try {
        acl = (MutableAcl) mutableAclService.readAclById(oi);
    } catch (NotFoundException nfe) {
        acl = mutableAclService.createAcl(oi);
    }

    // Insérer l'ACE (permission accordée)
    acl.insertAce(acl.getEntries().size(), permission, sid, true);
    mutableAclService.updateAcl(acl);
}

// Exemple d'appel après sauvegarde d'un nouvel objet
MyDomainObject newDoc = repository.save(new MyDomainObject(/*...*/));
grantPermissions(newDoc, SecurityContextHolder.getContext().getAuthentication().getName(), BasePermission.ADMINISTRATION); // Donner tous les droits au créateur
grantPermissions(newDoc, "collaborator_user", BasePermission.WRITE); // Donner droit d'écriture à un autre user

Vérifier les permissions : La manière la plus intégrée est d'utiliser les annotations @PreAuthorize ou @PostAuthorize avec l'expression SpEL hasPermission(). Cette expression est évaluée par le AclPermissionEvaluator configuré précédemment.

  • hasPermission(Object target, Object permission) : Vérifie si l'utilisateur courant a la permission donnée sur l'objet 'target'. Souvent utilisé avec @PostAuthorize où 'target' est la valeur retournée (returnObject).
  • hasPermission(Object targetId, String targetType, Object permission) : Vérifie la permission sur un objet identifié par son ID et son type. Utile avec @PreAuthorize quand l'objet n'est pas encore chargé mais que son ID est disponible comme paramètre de méthode.
@Service
public class DocumentService {

    @Autowired
    private DocumentRepository repository;

    // Vérifie avant l'exécution si l'utilisateur a le droit READ sur le document
    @PreAuthorize("hasPermission(#documentId, 'com.example.myapp.domain.Document', 'READ')")
    public Document findDocumentById(Long documentId) {
        return repository.findById(documentId).orElseThrow(() -> new NotFoundException());
    }

    // Vérifie après l'exécution si l'utilisateur a le droit WRITE sur le document retourné
    @PostAuthorize("hasPermission(returnObject, 'WRITE')")
    public Document updateDocument(Document document) {
        // ... logique de mise à jour
        return repository.save(document);
    }

    @PreAuthorize("hasPermission(#document, 'DELETE')")
    public void deleteDocument(Document document) {
        repository.delete(document);
        // N'oubliez pas de supprimer l'ACL associée !
        // mutableAclService.deleteAcl(new ObjectIdentityImpl(document), true);
    }
}

Il est aussi possible de vérifier les permissions programmatiquement en injectant AclService et en utilisant sa méthode readAclById() puis en appelant isGranted() sur l'ACL récupérée, mais l'approche déclarative avec les annotations est souvent préférée pour la clarté.

Considérations sur les performances et alternatives

L'utilisation intensive des ACLs peut avoir un impact sur les performances, car chaque vérification de permission peut potentiellement entraîner plusieurs requêtes à la base de données (pour charger l'ACL, les ACEs, résoudre l'héritage, etc.). C'est pourquoi la mise en place d'un cache (AclCache) est quasi indispensable en production.

La complexité de la configuration et de la gestion des ACLs (création/suppression des entrées ACL en même temps que les objets métier, gestion des tables) représente également une charge de développement non négligeable. Il faut évaluer si le besoin de sécurité au niveau de l'instance justifie cet investissement.

Avant d'opter pour Spring Security ACL, considérez si des approches plus simples pourraient suffire :

  • Logique métier personnalisée : Intégrer les vérifications de permissions directement dans les services métier (par exemple, vérifier si l'utilisateur courant est le propriétaire de l'objet).
  • Modèles de permissions simplifiés : Utiliser des champs supplémentaires sur vos objets métier (par exemple, un champ 'ownerId' ou une liste 'authorizedUserIds') et baser les vérifications sur ces champs via SpEL dans @PreAuthorize/@PostAuthorize.

Spring Security ACL reste cependant la solution la plus standard et la plus robuste fournie par le framework pour implémenter un contrôle d'accès fin basé sur les instances d'objets dans les applications Java complexes.