Introdução ao Spring Security ACL
1. Introdução
Access Control List (ACL) é uma lista de permissões anexadas a um objeto. UmACL especifica quais identidades são concedidas a quais operações em um determinado objeto.
Spring Security _Access Control List_ éa Spring component which supports Domain Object Security. Simplificando, Spring ACL ajuda a definir permissões para usuário / função específica em um único objeto de domínio - em vez de em toda a placa, no nível típico por operação.
Por exemplo, um usuário com a funçãoAdmin pode ver (READ) e editar (WRITE) todas as mensagens em umCentral Notice Box, mas o usuário normal só pode ver as mensagens relacionadas a eles e não podem editar. Enquanto isso, outros usuários com a funçãoEditor podem ver e editar algumas mensagens específicas.
Portanto, diferentes usuários / funções têm permissões diferentes para cada objeto específico. Nesse caso,Spring ACL é capaz de realizar a tarefa. Exploraremos como configurar a verificação de permissão básica comSpring ACL neste artigo.
2. Configuração
2.1. Banco de dados ACL
Para usarSpring Security ACL, precisamos criar quatro tabelas obrigatórias em nosso banco de dados.
A primeira tabela éACL_CLASS, que armazena o nome da classe do objeto de domínio, as colunas incluem:
-
ID
-
CLASS: o nome da classe de objetos de domínio seguro, por exemplo:org.example.acl.persistence.entity.NoticeMessage
Em segundo lugar, precisamos da tabelaACL_SID, que nos permite identificar universalmente qualquer princípio ou autoridade no sistema. A tabela precisa de:
-
ID
-
SID: que é o nome de usuário ou nome da função. SID representaSecurity Identity
-
PRINCIPAL:0 ou1, para indicar que oSID correspondente é um principal (usuário, comomary, mike, jack…) ou uma autoridade (função, comoROLE_ADMIN, ROLE_USER, ROLE_EDITOR…)
A próxima tabela éACL_OBJECT_IDENTITY,, que armazena informações para cada objeto de domínio exclusivo:
-
ID
-
OBJECT_ID_CLASS: define a classe de objeto de domínio, tabela_ links to _ACL_CLASS
-
OBJECT_ID_IDENTITY: objetos de domínio podem ser armazenados em muitas tabelas, dependendo da classe. Portanto, esse campo armazena a chave primária do objeto de destino
-
PARENT_OBJECT: especifica o pai desteObject Identity nesta tabela
-
OWNER_SID:ID do proprietário do objeto, links para a tabelaACL_SID
-
ENTRIES_INHERITTING: seACL Entries deste objeto herda do objeto pai (ACL Entries são definidos na tabelaACL_ENTRY)
Finalmente, a permissão individual da lojaACL_ENTRY atribui a cadaSID em umObject Identity:
-
ID
-
ACL_OBJECT_IDENTITY: especifica a identidade do objeto, links para a tabelaACL_OBJECT_IDENTITY
-
ACE_ORDER: a ordem da entrada atual na listaACL entries dosObject Identity correspondentes
-
SID: o destinoSID ao qual a permissão é concedida ou negada, links para a tabelaACL_SID
-
MASK: a máscara de bit inteiro que representa a permissão real sendo concedida ou negada
-
GRANTING: valor 1 significa concessão, valor0 significa negação
-
AUDIT_SUCCESSeAUDIT_FAILURE: para fins de auditoria
2.2. Dependência
Para poder usarSpring ACL em nosso projeto, vamos primeiro definir nossas dependências:
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 requer um cache para armazenarObject Identity eACL entries, então usaremosEhcache aqui. E, para suportarEhcache emSpring,, também precisamos dospring-context-support.
Quando não estiver trabalhando com o Spring Boot, precisamos adicionar versões explicitamente. Eles podem ser verificados no Maven Central:spring-security-acl,spring-security-config,spring-context-support,ehcache-core.
2.3. Configuração relacionada a ACL
Precisamos proteger todos os métodos que retornam objetos de domínio seguro, ou fazem alterações no objeto, habilitandoGlobal Method Security:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration
extends GlobalMethodSecurityConfiguration {
@Autowired
MethodSecurityExpressionHandler
defaultMethodSecurityExpressionHandler;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return defaultMethodSecurityExpressionHandler;
}
}
Vamos também ativarExpression-Based Access Control definindoprePostEnabled paratrue para usarSpring Expression Language (SpEL). Além disso,, precisamos de um manipulador de expressão com suporteACL:
@Bean
public MethodSecurityExpressionHandler
defaultMethodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler
= new DefaultMethodSecurityExpressionHandler();
AclPermissionEvaluator permissionEvaluator
= new AclPermissionEvaluator(aclService());
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
Portanto,, atribuímosAclPermissionEvaluator aoDefaultMethodSecurityExpressionHandler. O avaliador precisa de umMutableAclService para carregar as configurações de permissão e as definições do objeto de domínio do banco de dados.
Para simplificar, usamos oJdbcMutableAclService fornecido:
@Bean
public JdbcMutableAclService aclService() {
return new JdbcMutableAclService(
dataSource, lookupStrategy(), aclCache());
}
Como seu nome,JdbcMutableAclService usaJDBCTemplate para simplificar o acesso ao banco de dados. Ele precisa deDataSource (forJDBCTemplate),LookupStrategy (fornece uma pesquisa otimizada ao consultar o banco de dados) e umAclCache (cachingACLEntrieseObject Identity).
Novamente, para simplificar, usamos osBasicLookupStrategyeEhCacheBasedAclCache fornecidos.
@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()
);
}
Aqui, oAclAuthorizationStrategy é responsável por concluir se um usuário atual possui todas as permissões necessárias em determinados objetos ou não.
Ele precisa do suporte dePermissionGrantingStrategy,, que define a lógica para determinar se uma permissão é concedida a umSID específico.
3. Segurança de método com Spring ACL
Até agora, fizemos todas as configurações necessárias.. Agora podemos colocar a regra de verificação necessária em nossos métodos seguros.
Por padrão,Spring ACL se refere à classeBasePermission para todas as permissões disponíveis. Basicamente, temos permissãoREAD, WRITE, CREATE, DELETEeADMINISTRATION.
Vamos tentar definir algumas regras de segurança:
@PostFilter("hasPermission(filterObject, 'READ')")
List findAll();
@PostAuthorize("hasPermission(returnObject, 'READ')")
NoticeMessage findById(Integer id);
@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage);
Após a execução do métodofindAll(),@PostFilter será disparado. A regra necessáriahasPermission(filterObject, ‘READ'), significa retornar apenas aquelesNoticeMessage para os quais o usuário atual tem permissãoREAD.
Da mesma forma,@PostAuthorize é disparado após a execução do métodofindById(), certifique-se de retornar apenas o objetoNoticeMessage se o usuário atual tiver permissãoREAD nele. Caso contrário, o sistema lançará umAccessDeniedException.
Por outro lado, o sistema dispara a anotação@PreAuthorize antes de invocar o métodosave(). Ele decidirá onde o método correspondente pode ser executado ou não. Caso contrário,AccessDeniedException será lançado.
4. Em ação
Agora vamos testar todas essas configurações usandoJUnit. Usaremos o banco de dadosH2 para manter a configuração o mais simples possível.
Precisamos adicionar:
com.h2database
h2
org.springframework
spring-test
test
org.springframework.security
spring-security-test
test
4.1. O cenário
Neste cenário, teremos dois usuários (manager, hr) e uma função de usuário (ROLE_EDITOR),, então nossoacl_sid será:
INSERT INTO acl_sid (id, principal, sid) VALUES
(1, 1, 'manager'),
(2, 1, 'hr'),
(3, 0, 'ROLE_EDITOR');
Então, precisamos declarar a classeNoticeMessage emacl_class. E três instâncias da classeNoticeMessage serão inseridas emsystem_message.
Além disso, os registros correspondentes para essas 3 instâncias devem ser declarados emacl_object_identity:
INSERT INTO acl_class (id, class) VALUES
(1, 'org.example.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);
Inicialmente, concedemos permissõesREADeWRITE no primeiro objeto (id =1) para o usuáriomanager. Enquanto isso, qualquer usuário comROLE_EDITOR terá permissãoREAD em todos os três objetos, mas possuirá apenas permissãoWRITE no terceiro objeto (id=3). Além disso, o usuáriohr terá apenas permissãoREAD no segundo objeto.
Aqui, como usamos a classeSpring ACLBasePermission padrão para verificação de permissão, o valor da máscara da permissãoREAD será 1 e o valor da máscara da permissãoWRITE será 2 . Nossos dados emacl_entry serão:
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. Caso de teste
Primeiramente, tentamos chamar o métodofindAll.
Como nossa configuração, o método retorna apenas aquelesNoticeMessage nos quais o usuário tem permissãoREAD.
Portanto, esperamos que a lista de resultados contenha apenas a primeira mensagem:
@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());
}
Em seguida, tentamos chamar o mesmo método com qualquer usuário que tenha a função -ROLE_EDITOR. Observe que, neste caso, esses usuários têm permissãoREAD em todos os três objetos.
Portanto, esperamos que a lista de resultados contenha todas as três mensagens:
@Test
@WithMockUser(roles = {"EDITOR"})
public void
givenRoleEditor_whenFindAllMessage_thenReturn3Message(){
List details = repo.findAll();
assertNotNull(details);
assertEquals(3,details.size());
}
Em seguida, usando o usuáriomanager, tentaremos obter a primeira mensagem por id e atualizar seu conteúdo - o que deve funcionar bem:
@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());
}
Mas se qualquer usuário com a funçãoROLE_EDITOR atualizar o conteúdo da primeira mensagem - nosso sistema lançará umAccessDeniedException:
@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);
}
Da mesma forma, o usuáriohr pode encontrar a segunda mensagem por id, mas não conseguirá atualizá-la:
@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. Conclusão
Analisamos a configuração básica e o uso deSpring ACL neste artigo.
Como sabemos,Spring ACL requeria tabelas específicas para o gerenciamento de objetos, princípios / autoridade e configuração de permissões. Todas as interações com essas tabelas, especialmente a ação de atualização, devem passar porAclService. Exploraremos este serviço para ações básicasCRUD em um artigo futuro.
Por padrão, estamos restritos à permissão predefinida na classeBasePermission.
Finalmente, a implementação deste tutorial pode ser encontradaover on Github.