Introduction à Spring Security ACL

1. Introduction

La liste de contrôle d'accès ( ACL) est une liste d'autorisations attachées à un objet. Une ACL spécifie quelles identités reçoivent quelles opérations sur un objet donné.

Spring Security Access Control List est un composant Spring qui prend en charge la sécurité des objets de domaine. En termes simples, Spring ACL aide à définir des autorisations pour un utilisateur / un rôle spécifique sur un seul objet de domaine - au lieu de le faire de manière générale, au niveau typique par opération.

Par exemple, un utilisateur avec le rôle Admin peut voir ( LIRE) et éditer ( ÉCRIRE) tous les messages sur une boîte de notification centrale , mais l'utilisateur normal ne peut voir que les messages, s'y rapporter et ne peut pas les modifier. Pendant ce temps, d'autres utilisateurs avec le rôle Editeur peuvent voir et modifier certains messages spécifiques.

Par conséquent, différents utilisateurs / rôles ont des autorisations différentes pour chaque objet spécifique. Dans ce cas, Spring ACL est capable d'accomplir la tâche. Nous allons explorer comment configurer la vérification des autorisations de base avec Spring ACL dans cet article.

2. Configuration

2.1. Base de données ACL

Pour utiliser Spring Security ACL , nous devons créer quatre tables obligatoires dans notre base de données.

La première table est ACL_CLASS , qui stocke le nom de classe de l'objet de domaine, les colonnes incluent:

  • ID
  • CLASS: le nom de classe des objets de domaine sécurisé, par exemple: com.baeldung.acl.persistence.entity.NoticeMessage

Deuxièmement, nous avons besoin de la table ACL_SID qui nous permet d'identifier universellement tout principe ou autorité dans le système. La table a besoin de:

  • ID
  • SID: qui est le nom d'utilisateur ou le nom du rôle. SID signifie Security Identity
  • PRINCIPAL: 0 ou 1 , pour indiquer que le SID correspondant est un principal (utilisateur, tel que mary, mike, jack… ) ou une autorité (rôle, tel que ROLE_ADMIN, ROLE_USER, ROLE_EDITOR… )

La table suivante est ACL_OBJECT_IDENTITY, qui stocke des informations pour chaque objet de domaine unique:

  • ID
  • OBJECT_ID_CLASS: définir la classe d'objets du domaine,liens vers la table ACL_CLASS
  • OBJECT_ID_IDENTITY: les objets du domaine peuvent être stockés dans de nombreuses tables en fonction de la classe. Par conséquent, ce champ stocke la clé primaire de l'objet cible
  • PARENT_OBJECT: spécifiez le parent de cette identité d'objet dans cette table
  • OWNER_SID: ID du propriétaire de l'objet, liens vers la table ACL_SID
  • ENTRIES_INHERITTING: si les entrées ACL de cet objet héritent de l'objet parent (les entrées ACL sont définies dans la table ACL_ENTRY )

Enfin, l' autorisation individuelle de magasin ACL_ENTRY est attribuée à chaque SID sur une identité d'objet :

  • ID
  • ACL_OBJECT_IDENTITY: spécifiez l'identité de l'objet, liens vers la table ACL_OBJECT_IDENTITY
  • ACE_ORDER: l'ordre de l'entrée courante dans la liste des entrées ACL de l' identité d'objet correspondante
  • SID: le SID cible auquel l'autorisation est accordée ou refusée, liens vers la table ACL_SID
  • MASK: le masque de bits entier qui représente l'autorisation réelle accordée ou refusée
  • GRANTING: la valeur 1 signifie l'octroi, la valeur 0 signifie le refus
  • AUDIT_SUCCESS et AUDIT_FAILURE : à des fins d'audit

2.2. Dépendance

Pour pouvoir utiliser Spring ACL dans notre projet, définissons d'abord nos dépendances:

 org.springframework.security spring-security-acl   org.springframework.security spring-security-config   org.springframework spring-context-support   net.sf.ehcache ehcache-core 2.6.11 

Spring ACL nécessite un cache pour stocker l' identité d'objet et les entrées ACL , nous allons donc utiliser Ehcache ici. Et, pour prendre en charge Ehcache au printemps, nous avons également besoin du support spring-context.

Lorsque vous ne travaillez pas avec Spring Boot, nous devons ajouter des versions explicitement. Ceux-ci peuvent être vérifiés sur Maven Central: spring-security-acl, spring-security-config, spring-context-support, ehcache-core.

2.3. Configuration liée à l'ACL

Nous devons sécuriser toutes les méthodes qui renvoient des objets de domaine sécurisés, ou apportent des modifications à l'objet, en activant Global Method Security:

@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class AclMethodSecurityConfiguration extends GlobalMethodSecurityConfiguration { @Autowired MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler; @Override protected MethodSecurityExpressionHandler createExpressionHandler() { return defaultMethodSecurityExpressionHandler; } }

Nous allons également permettre l' expression contrôle d' accès basé en mettant prePostEnabled à vrai pour utiliser l' expression Spring Langue (SPEL) . De plus , nous avons besoin d'un gestionnaire d'expressions avec prise en charge de l' ACL :

@Bean public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() { DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService()); expressionHandler.setPermissionEvaluator(permissionEvaluator); return expressionHandler; }

Par conséquent , nous affectons AclPermissionEvaluator à DefaultMethodSecurityExpressionHandler . L'évaluateur a besoin d'un MutableAclService pour charger les paramètres d'autorisation et les définitions d'objet de domaine à partir de la base de données.

Pour plus de simplicité, nous utilisons le JdbcMutableAclService fourni :

@Bean public JdbcMutableAclService aclService() { return new JdbcMutableAclService( dataSource, lookupStrategy(), aclCache()); }

Comme son nom, le JdbcMutableAclService utilise JDBCTemplate pour simplifier l'accès à la base de données. Il a besoin d'un DataSource ( pour JDBCTemplate) , LookupStrategy (fournit une recherche optimisée lors de l'interrogation de la base de données) et d'un AclCache ( mise en cache des entrées ACL et de l' identité d'objet) .

Encore une fois, pour plus de simplicité, nous utilisons BasicLookupStrategy et EhCacheBasedAclCache fournis .

@Autowired DataSource dataSource; @Bean public AclAuthorizationStrategy aclAuthorizationStrategy() { return new AclAuthorizationStrategyImpl( new SimpleGrantedAuthority("ROLE_ADMIN")); } @Bean public PermissionGrantingStrategy permissionGrantingStrategy() { return new DefaultPermissionGrantingStrategy( new ConsoleAuditLogger()); } @Bean public EhCacheBasedAclCache aclCache() { return new EhCacheBasedAclCache( aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy() ); } @Bean public EhCacheFactoryBean aclEhCacheFactoryBean() { EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean(); ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject()); ehCacheFactoryBean.setCacheName("aclCache"); return ehCacheFactoryBean; } @Bean public EhCacheManagerFactoryBean aclCacheManager() { return new EhCacheManagerFactoryBean(); } @Bean public LookupStrategy lookupStrategy() { return new BasicLookupStrategy( dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger() ); } 

Ici, l' AclAuthorizationStrategy est chargé de déterminer si un utilisateur actuel possède toutes les autorisations requises sur certains objets ou non.

Il a besoin de la prise en charge de PermissionGrantingStrategy, qui définit la logique permettant de déterminer si une autorisation est accordée à un SID particulier .

3. Sécurité des méthodes avec Spring ACL

Jusqu'à présent, nous avons effectué toute la configuration nécessaire . Nous pouvons maintenant mettre la règle de vérification requise sur nos méthodes sécurisées.

Par défaut, Spring ACL fait référence à la classe BasePermission pour toutes les autorisations disponibles. Fondamentalement, nous avons une autorisation LIRE, ÉCRIRE, CRÉER, SUPPRIMER et ADMINISTRATION .

Essayons de définir quelques règles de sécurité:

@PostFilter("hasPermission(filterObject, 'READ')") List findAll(); @PostAuthorize("hasPermission(returnObject, 'READ')") NoticeMessage findById(Integer id); @PreAuthorize("hasPermission(#noticeMessage, 'WRITE')") NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage);

After the execution of findAll() method, @PostFilter will be triggered. The required rule hasPermission(filterObject, ‘READ'), means returning only those NoticeMessage which current user has READ permission on.

Similarly, @PostAuthorize is triggered after the execution of findById() method, make sure only return the NoticeMessage object if the current user has READ permission on it. If not, the system will throw an AccessDeniedException.

On the other side, the system triggers the @PreAuthorize annotation before invoking the save() method. It will decide where the corresponding method is allowed to execute or not. If not, AccessDeniedException will be thrown.

4. In Action

Now we gonna test all those configurations using JUnit. We'll use H2 database to keep configuration as simple as possible.

We'll need to add:

 com.h2database h2   org.springframework spring-test test   org.springframework.security spring-security-test test 

4.1. The Scenario

In this scenario, we'll have two users (manager, hr) and a one user role (ROLE_EDITOR), so our acl_sid will be:

INSERT INTO acl_sid (id, principal, sid) VALUES (1, 1, 'manager'), (2, 1, 'hr'), (3, 0, 'ROLE_EDITOR');

Then, we need to declare NoticeMessage class in acl_class. And three instances of NoticeMessage class will be inserted in system_message.

Moreover, corresponding records for those 3 instances must be declared in acl_object_identity:

INSERT INTO acl_class (id, class) VALUES (1, 'com.baeldung.acl.persistence.entity.NoticeMessage'); INSERT INTO system_message(id,content) VALUES (1,'First Level Message'), (2,'Second Level Message'), (3,'Third Level Message'); INSERT INTO acl_object_identity (id, object_id_class, object_id_identity, parent_object, owner_sid, entries_inheriting) VALUES (1, 1, 1, NULL, 3, 0), (2, 1, 2, NULL, 3, 0), (3, 1, 3, NULL, 3, 0);

Initially, we grant READ and WRITE permissions on the first object (id =1) to the user manager. Meanwhile, any user with ROLE_EDITOR will have READ permission on all three objects but only possess WRITE permission on the third object (id=3). Besides, user hr will have only READ permission on the second object.

Here, because we use default Spring ACLBasePermission class for permission checking, the mask value of the READ permission will be 1, and the mask value of WRITE permission will be 2. Our data in acl_entry will be:

INSERT INTO acl_entry (id, acl_object_identity, ace_order, sid, mask, granting, audit_success, audit_failure) VALUES (1, 1, 1, 1, 1, 1, 1, 1), (2, 1, 2, 1, 2, 1, 1, 1), (3, 1, 3, 3, 1, 1, 1, 1), (4, 2, 1, 2, 1, 1, 1, 1), (5, 2, 2, 3, 1, 1, 1, 1), (6, 3, 1, 3, 1, 1, 1, 1), (7, 3, 2, 3, 2, 1, 1, 1);

4.2. Test Case

First of all, we try to call the findAll method.

As our configuration, the method returns only those NoticeMessage on which the user has READ permission.

Hence, we expect the result list contains only the first message:

@Test @WithMockUser(username = "manager") public void givenUserManager_whenFindAllMessage_thenReturnFirstMessage(){ List details = repo.findAll(); assertNotNull(details); assertEquals(1,details.size()); assertEquals(FIRST_MESSAGE_ID,details.get(0).getId()); }

Then we try to call the same method with any user which has the role – ROLE_EDITOR. Note that, in this case, these users have the READ permission on all three objects.

Hence, we expect the result list will contain all three messages:

@Test @WithMockUser(roles = {"EDITOR"}) public void givenRoleEditor_whenFindAllMessage_thenReturn3Message(){ List details = repo.findAll(); assertNotNull(details); assertEquals(3,details.size()); }

Next, using the manager user, we'll try to get the first message by id and update its content – which should all work fine:

@Test @WithMockUser(username = "manager") public void givenUserManager_whenFind1stMessageByIdAndUpdateItsContent_thenOK(){ NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID); assertNotNull(firstMessage); assertEquals(FIRST_MESSAGE_ID,firstMessage.getId()); firstMessage.setContent(EDITTED_CONTENT); repo.save(firstMessage); NoticeMessage editedFirstMessage = repo.findById(FIRST_MESSAGE_ID); assertNotNull(editedFirstMessage); assertEquals(FIRST_MESSAGE_ID,editedFirstMessage.getId()); assertEquals(EDITTED_CONTENT,editedFirstMessage.getContent()); }

But if any user with the ROLE_EDITOR role updates the content of the first message – our system will throw an AccessDeniedException:

@Test(expected = AccessDeniedException.class) @WithMockUser(roles = {"EDITOR"}) public void givenRoleEditor_whenFind1stMessageByIdAndUpdateContent_thenFail(){ NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID); assertNotNull(firstMessage); assertEquals(FIRST_MESSAGE_ID,firstMessage.getId()); firstMessage.setContent(EDITTED_CONTENT); repo.save(firstMessage); }

Similarly, the hr user can find the second message by id, but will fail to update it:

@Test @WithMockUser(username = "hr") public void givenUsernameHr_whenFindMessageById2_thenOK(){ NoticeMessage secondMessage = repo.findById(SECOND_MESSAGE_ID); assertNotNull(secondMessage); assertEquals(SECOND_MESSAGE_ID,secondMessage.getId()); } @Test(expected = AccessDeniedException.class) @WithMockUser(username = "hr") public void givenUsernameHr_whenUpdateMessageWithId2_thenFail(){ NoticeMessage secondMessage = new NoticeMessage(); secondMessage.setId(SECOND_MESSAGE_ID); secondMessage.setContent(EDITTED_CONTENT); repo.save(secondMessage); }

5. Conclusion

Nous avons passé en revue la configuration de base et l'utilisation de Spring ACL dans cet article.

Comme nous le savons, Spring ACL nécessitait des tables spécifiques pour gérer les paramètres d'objet, de principe / d'autorité et d'autorisation. Toutes les interactions avec ces tables, en particulier l'action de mise à jour, doivent passer par AclService. Nous explorerons ce service pour les actions CRUD de base dans un prochain article.

Par défaut, nous sommes limités à l'autorisation prédéfinie dans la classe BasePermissio n.

Enfin, l'implémentation de ce tutoriel peut être trouvée sur Github.