Introduzione a Spring Security ACL

1. Introduzione

L'elenco di controllo degli accessi ( ACL) è un elenco di autorizzazioni associate a un oggetto. Un ACL specifica a quali identità vengono concesse le operazioni su un determinato oggetto.

Spring Security Access Control List è un componente Spring che supporta Domain Object Security. In poche parole, Spring ACL aiuta a definire le autorizzazioni per un utente / ruolo specifico su un singolo oggetto di dominio, invece che su tutta la linea, al livello tipico per operazione.

Ad esempio, un utente con il ruolo di amministratore può vedere ( LEGGERE) e modificare ( SCRIVERE) tutti i messaggi in una casella centrale di notifica , ma solo l'utente normale può vedere i messaggi, collegarsi ad essi e non può modificare. Nel frattempo, altri utenti con il ruolo Editor possono vedere e modificare alcuni messaggi specifici.

Pertanto, un utente / ruolo diverso ha autorizzazioni diverse per ogni oggetto specifico. In questo caso, Spring ACL è in grado di svolgere l'attività. In questo articolo esploreremo come impostare il controllo delle autorizzazioni di base con Spring ACL .

2. Configurazione

2.1. Database ACL

Per utilizzare Spring Security ACL , dobbiamo creare quattro tabelle obbligatorie nel nostro database.

La prima tabella è ACL_CLASS , che memorizza il nome della classe dell'oggetto dominio, le colonne includono:

  • ID
  • CLASS: il nome della classe degli oggetti del dominio protetto, ad esempio: com.baeldung.acl.persistence.entity.NoticeMessage

In secondo luogo, abbiamo bisogno della tabella ACL_SID che ci consente di identificare universalmente qualsiasi principio o autorità nel sistema. Il tavolo ha bisogno di:

  • ID
  • SID: che è il nome utente o il nome del ruolo. SID sta per Security Identity
  • PRINCIPALE: 0 o 1 , per indicare che il SID corrispondente è un principale (utente, come mary, mike, jack ... ) o un'autorità (ruolo, come ROLE_ADMIN, ROLE_USER, ROLE_EDITOR ... )

La tabella successiva è ACL_OBJECT_IDENTITY, che memorizza le informazioni per ogni oggetto di dominio univoco:

  • ID
  • OBJECT_ID_CLASS: definisce la classe dell'oggetto del dominio,collegamenti alla tabella ACL_CLASS
  • OBJECT_ID_IDENTITY: gli oggetti del dominio possono essere memorizzati in molte tabelle a seconda della classe. Quindi, questo campo memorizza la chiave primaria dell'oggetto di destinazione
  • PARENT_OBJECT: specifica il genitore di questa Identità oggetto all'interno di questa tabella
  • OWNER_SID: ID del proprietario dell'oggetto, link alla tabella ACL_SID
  • ENTRIES_INHERITTING: se le voci ACL di questo oggetto ereditano dall'oggetto padre ( le voci ACL sono definite nella tabella ACL_ENTRY )

Infine, ACL_ENTRY archivia le singole autorizzazioni assegnate a ciascun SID su un'identità oggetto :

  • ID
  • ACL_OBJECT_IDENTITY: specifica l'identità dell'oggetto, collega alla tabella ACL_OBJECT_IDENTITY
  • ACE_ORDER: l'ordine della voce corrente nell'elenco delle voci ACL dell'identità dell'oggetto corrispondente
  • SID: il SID di destinazione a cui viene concessa o negata l'autorizzazione, si collega alla tabella ACL_SID
  • MASK: la maschera di bit intero che rappresenta l'autorizzazione effettiva concessa o negata
  • CONCESSIONE: valore 1 significa concessione, valore 0 significa negare
  • AUDIT_SUCCESS e AUDIT_FAILURE : a scopo di controllo

2.2. Dipendenza

Per poter utilizzare Spring ACL nel nostro progetto, definiamo prima le nostre dipendenze:

 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 richiede una cache per memorizzare l' identità dell'oggetto e le voci ACL , quindi utilizzeremo Ehcache qui. E, per supportare Ehcache in primavera, abbiamo anche bisogno del supporto del contesto primaverile.

Quando non si lavora con Spring Boot, è necessario aggiungere le versioni in modo esplicito. Questi possono essere controllati su Maven Central: spring-security-acl, spring-security-config, spring-context-support, ehcache-core.

2.3. Configurazione relativa a ACL

È necessario proteggere tutti i metodi che restituiscono oggetti di dominio protetti o apportare modifiche all'oggetto abilitando Global Method Security:

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

Diamo consentono inoltre Expression-Based Access Control impostando prePostEnabled al vero di utilizzare Primavera Expression Language (SPEL) . Inoltre , abbiamo bisogno di un gestore di espressioni con supporto ACL :

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

Quindi , assegniamo AclPermissionEvaluator a DefaultMethodSecurityExpressionHandler . Il programma di valutazione necessita di un MutableAclService per caricare le impostazioni di autorizzazione e le definizioni degli oggetti di dominio dal database.

Per semplicità, utilizziamo il JdbcMutableAclService fornito :

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

Come nome, JdbcMutableAclService utilizza JDBCTemplate per semplificare l'accesso al database. Richiede un DataSource ( per JDBCTemplate) , LookupStrategy (fornisce una ricerca ottimizzata durante l'interrogazione del database) e un AclCache ( memorizzazione nella cache delle voci ACL e dell'identità dell'oggetto) .

Ancora una volta, per semplicità, utilizziamo BasicLookupStrategy e EhCacheBasedAclCache forniti .

@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() ); } 

Qui, AclAuthorizationStrategy ha il compito di stabilire se un utente corrente possiede o meno tutte le autorizzazioni richieste su determinati oggetti.

Ha bisogno del supporto di PermissionGrantingStrategy, che definisce la logica per determinare se un'autorizzazione è concessa a un particolare SID .

3. Protezione del metodo con Spring ACL

Finora abbiamo eseguito tutte le configurazioni necessarie . Ora possiamo inserire la regola di controllo richiesta sui nostri metodi protetti.

Per impostazione predefinita, Spring ACL fa riferimento alla classe BasePermission per tutte le autorizzazioni disponibili. Fondamentalmente, abbiamo un'autorizzazione READ, WRITE, CREATE, DELETE e ADMINISTRATION .

Proviamo a definire alcune regole di sicurezza:

@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

In questo articolo abbiamo esaminato la configurazione di base e l'utilizzo di Spring ACL .

Come sappiamo, Spring ACL richiedeva tabelle specifiche per la gestione di oggetti, principio / autorità e impostazione dei permessi. Tutte le interazioni con queste tabelle, in particolare l'aggiornamento dell'azione, devono passare tramite AclService. Esploreremo questo servizio per le azioni CRUD di base in un prossimo articolo.

Per impostazione predefinita, siamo limitati a autorizzazioni predefinite nella classe BasePermissio n.

Infine, l'implementazione di questo tutorial può essere trovata su Github.