Access Control List: Protegendo o acesso a seus objetos

O Spring Security é um dos módulos mais usados do Spring e sem dúvida cumpre o seu papel muito bem. Ele é bastante usado para restringirmos o acesso aos métodos e urls da nossa aplicação baseado nos perfis dos usuários do sistema. Uma situação mais complicada pode aparecer quando dois usuários possuem um perfil que permite o acesso a uma mesma url, por exemplo um endereço que exibe o detalhe de uma prova dele em um sistema de provas online. Segue abaixo o exemplo:

  /prova/resolucao/1

E se o outro usuário logado também tentar acessar o mesmo endereço de detalhe? Como você vai negar o acesso? Provavelmente acabaríamos escrevendo um código parecido com o que segue.

	Usuario usuarioLogado = ...;
	ResolucaoProva resolucao = resolucaoProvaDao.carrega(idResolucao);
	if(!resolucao.pertence(usuarioLogado)){
		throw new SemAutorizacaoException("espertinho...");
	}

Essa é uma solução perfeitamente válida. O ponto negativo é que você não vai reaproveitar para os outros sistemas. Além do mais, esse sistema de permissões pode ficar mais complexo com usuários podendo ver detalhes de outros usuários, mas sem possibilidade edição por exemplo. Tudo vai depender do tipo de aplicação que você estiver desenvolvendo.

Para amenizar essa complexidade, o Spring Security tem um módulo chamado de ACL(Access Control List). O objetivo dele é justamente generalizar essa implementação nos dando a possibilidade de aplicar regras de acesso para qualquer tipo de objeto! Essa semana eu e Rafael Rollo utilizamos ele em um projeto dentro da Caelum. O objetivo do post é justamente mostrar o código necessário para deixar tudo funcionando. Respire bastante, porque não é pouco código não.

Vamos começar pela parte mais interessante, pelo menos para mim, que é a de inserir as permissões de acesso para determinado objeto. Dê uma olhada no código abaixo:

	@Autowired
	private Permissoes permissoes;

	...

	@RequestMapping(value = "/finaliza", method = RequestMethod.POST)
	public String finaliza(HttpSession session,@AuthenticationPrincipal 
				Usuario usuarioLogado) {

		ResolucaoFinal resolucaoFinal = resolucaoEmAndamento
				.geraResolucaoFinal();
		resolucaoFinalDao.save(resolucaoFinal);

		//essa classe isola o código de adicionar permissões
		permissoes.adiciona(usuarioLogado,resolucaoFinal,BasePermission.READ);

		return "redirect:/resolucao/finalizada";
	}
	@Service
	public class Permissoes {
		
		@Autowired
		private MutableAclService mutableAclService;	

		public void adiciona(UserDetails user, Object objeto, 
                      Permission... permissoes) {
	        ObjectIdentity identity = new ObjectIdentityImpl(objeto);
	        MutableAcl acl = mutableAclService.createAcl(identity);
	        PrincipalSid principalSid = new PrincipalSid(user.getUsername());
	        for (Permission permissao : permissoes) {
	        	acl.insertAce(acl.getEntries().size(), permissao, principalSid, true);			
			}
	        mutableAclService.updateAcl(acl);	
		}

	}

Vamos focar na classe Permissoes, já que nela está acontecendo a parte que nos interessa. A classe MutableAclService é a nossa porta de entrada para criar, atualizar e ler as permissões de acesso a um objeto. Tanto que nesse pequeno trecho de código usamos dois métodos dela:

  1. createAcl
  2. updateAcl

Vamos analisá-los por partes, começando pelo createAcl. 

		public void adiciona(UserDetails user, Object objeto, 
                        Permission... permissoes) {
	        ObjectIdentity identity = new ObjectIdentityImpl(objeto);
	        MutableAcl acl = mutableAclService.createAcl(identity);
                ...

O createAcl espera como argumento um ObjectIdentity, que é justamente a representação do objeto que vai ter o acesso controlado. O retorno deste método é um objeto do tipo MutableAcl, a abstração para as permissões de um objetoComo você já deve ter notado, o construtor do ObjectIdentity recebe um Object como parâmetro, já que qualquer tipo de objeto pode ser protegido. Um ponto importante é que esse Object deve ter um método chamado getId, cujo retorno será usado como chave estrangeira nas tabelas que serão criadas(ainda vamos comentar sobre isso). Caso sua classe não possua um método chamado getId, você pode usar o outro construtor que recebe além do objeto, um id como argumento.

Para falarmos do updateAcl, precisamos primeiro dar uma olhada no método insertAcl da classe MutableAcl. Através dele é que vamos adicionando permissões para o nosso objeto.

	        MutableAcl acl = mutableAclService.createAcl(identity);
	        PrincipalSid principalSid = new PrincipalSid(user.getUsername());
	        for (Permission permissao : permissoes) {
	           acl.insertAce(acl.getEntries().size(), permissao, principalSid, true);			
		}

Para ser bem sincero, essa assinatura de método é um pouco estranha para mim :/. Vamos começar pelos argumentos que fazem mais sentido, o segundo e o terceiro. O segundo argumento é um objeto cuja classe implementa a interface Permission, que é a abstração de uma permissão dentro do ACL. Ele já tem uma implementação base que é tranquila para a gente usar.

	public class BasePermission extends AbstractPermission {
		public static final Permission READ = new BasePermission(1 << 0, 'R'); // 1
		public static final Permission WRITE = new BasePermission(1 << 1, 'W'); // 2
		public static final Permission CREATE = new BasePermission(1 << 2, 'C'); // 4
		public static final Permission DELETE = new BasePermission(1 << 3, 'D'); // 8
		public static final Permission ADMINISTRATION = new BasePermission(1 << 4, 'A'); // 16

		protected BasePermission(int mask) {
			super(mask);
		}

		protected BasePermission(int mask, char code) {
			super(mask, code);
		}
	}

Caso você esteja questionando o motivo da permissão ser representada por um int, bem vindo ao time :), neste link eles discutem sobre a decisão. O terceiro argumento do método é justamente o dono do objeto, que é representado pela classe PrincipalSid(Principal Security Identity).  Passamos como argumento no construtor dele um identificador desse “dono” que existe dentro do nosso sistema, no meu caso foi o email do usuário.

Pensando de maneira direta, os argumentos da permissão e do dono daquela permissão deveriam ser suficientes para definir a regra de acesso ao objeto, só que ainda temos outros dois. Vou tentar ser breve aqui, já que para mim eles não fazem muito sentido. A classe MutableAcl possui um outro método chamado updateAce, cuja assinatura segue abaixo:

    void updateAce(int aceIndex, Permission permission) throws NotFoundException;

Perceba que o primeiro argumento é um índice que é necessário para localizar a posição da permissão. Como para atualizar precisa do índice, para inserir você também precisar passar qual é a posição que você deseja adicionar a nova permissão. Essa é a razão do primeiro argumento do método insertAce ser um inteiro. O último argumento é um boolean para indicar se você realmente quer dar aquela permissão de acesso ao objeto. O que eu não entendo é: se você não quer dar permissão de acesso, não é melhor simplesmente não adicionar? Sei lá, devo estar me passando em alguma coisa aqui.

Com as permissões inseridas no objeto do tipo MutableAcl, podemos invocar o updateAcl da classe MutableAclService, para que tudo seja refletido no banco de dados.

Uma vez que as permissões estão associadas, você deve estar se perguntando como vamos proteger o acesso ao método. Dê uma olhada no código abaixo.

	@RequestMapping(value = "/prova/resolucao/{id}", method = RequestMethod.POST)
	@PreAuthorize("hasPermission(#id,'br.com.caelum.provas.models.ResolucaoFinal','read')")
	public String visualiza(@PathVariable("id") Integer id) {
		ResolucaoFinal resolucao = resolucaoDao.carrega(id);
		...
	}

Aqui fizemos uso das expressões suportadas pelo Spring. Por sinal esse é um tema que merece um post aqui no blog :). Temos acesso a função hasPermission que recebe três argumentos.

  1. O id objeto que está tentando ser acessado. Repare que usamos o #” para indicar que ele deve pegar esse valor do argumento de mesmo nome.
  2. A classe do objeto que ele precisa carregar.
  3. O nome da permissão que o usuário logado precisa ter. Esse nome precisa ser o mesmo da constante usada na hora de adicionar a permissão. Por default o ACL vai fazer o carregamento da permissão baseado nessa condição.

O Spring Security ACL vai verificar se o usuário logado tem acesso especificamente a este objeto. Essa é uma solução super genérica e que você pode aproveitar para qualquer projeto seu!

Para fechar, precisamos realizar as configurações necessárias para que tudo funcione. Essa parte toda eu vou deixar isolada em uma classe específica.

	@Configuration
	public class ConfiguracaoACL {

		@Autowired
		private DataSource dataSource;

		@Bean
		public AclAuthorizationStrategy aclAuthorization() {
			AclAuthorizationStrategyImpl aclAuthorizationStrategy = new AclAuthorizationStrategyImpl(
					new SimpleGrantedAuthority("ROLE_ADMIN"));				
			return aclAuthorizationStrategy;
		}

		@Bean
		public PermissionGrantingStrategy permissionGrantingStrategy() {
			PermissionGrantingStrategy grantingStrategy = new DefaultPermissionGrantingStrategy(
					new ConsoleAuditLogger());
			return grantingStrategy;
		}

		@Bean	
		public Ehcache ehCache() {
			CacheManager cacheManager = CacheManager.create();
			if (!cacheManager.cacheExists("aclCache")) {
				cacheManager.addCache("aclCache");
			}

			Ehcache ehCache = cacheManager.getEhcache("aclCache");
			return ehCache;
		}
			

		@Bean
		public AclCache aclCache(Ehcache ehCache, PermissionGrantingStrategy grantingStrategy,
				AclAuthorizationStrategy aclAuthorizationStrategy) {
			AclCache aclCache = new EhCacheBasedAclCache(ehCache, grantingStrategy,
					aclAuthorizationStrategy);

			return aclCache;
		}

		@Bean
		public BasicLookupStrategy basicLookupStrategy(AclCache aclCache,
				AclAuthorizationStrategy aclAuthorizationStrategy,
				PermissionGrantingStrategy grantingStrategy) {

			return new BasicLookupStrategy(dataSource, aclCache, aclAuthorizationStrategy,
					grantingStrategy);
		}

		@Bean
		public MutableAclService mutableAclService(BasicLookupStrategy lookupStrategy, AclCache aclCache) {
			JdbcMutableAclService service = new JdbcMutableAclService(dataSource, lookupStrategy,
					aclCache);
			service.setClassIdentityQuery("select last_insert_id()");
			service.setSidIdentityQuery("select last_insert_id()");
			return service;
		}

		@Bean
		public MethodSecurityExpressionHandler expressionHandler(PermissionEvaluator permissionEvaluator) {
			DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
			handler.setPermissionEvaluator(permissionEvaluator);
			return handler;
		}

		@Bean
		public PermissionEvaluator permissionEvaluator(MutableAclService aclService) {
			AclPermissionEvaluator aclPermissionEvaluator = new AclPermissionEvaluator(aclService);
			return aclPermissionEvaluator;
		}

	}

Aqui você precisa medir seus esforços. Não se pode negar que cada um desses métodos produz um objeto que é necessário para o funcionamento, mas não acho saudável gastar vários parágrafos explicando um por um. Vou deixar uma lista apenas com uma breve ideia :).

  1. LookupStrategy: dependência do JdbcMutableAclService para fazer buscas no banco pelas permissões.
  2. AclCache: Camada de cache para não ficar indo no banco o tempo todo.
  3. AclAuthorizationStrategy: Verifica se certa role pode alterar permissões de um objeto.
  4. PermissionGrantingStrategy: Verifica se certo usuário pode realizar uma operação sobre o objeto.
  5. PermissionEvalutaor: Necessária para suportar o hasPermission nos métodos.

Como é de praxe, aconselho que você olhe o Javadoc de todas elas, para saber exatamente do papel de cada uma. Aproveitando, já vou fazer uma atualização no SetupMyProject para suportar a configuração do ACL :). Para completar é necessário criar as tabelas necessárias para que o módulo do ACL funcione e, além disso, modificar sua classe de configuração do Spring Security e adicionar a annotation @EnableGlobalMethodSecurity(prePostEnabled=true). Essa annotation habilita a checagem da annotation @PreAuthorize :).

Para você não esquecer, também deixo as dependências que precisam ser adicionadas no pom.xml.

		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-acl</artifactId>
		</dependency>
		<!-- cache -->

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>

		<dependency>
			<groupId>net.sf.ehcache</groupId>
			<artifactId>ehcache</artifactId>
		</dependency>

Pronto! O post foi um pouco longo, mas também o uso e configuração dele não é lá uma das coisas mais fáceis. E esse foi só o primeiro deste ano, se preparem para mais. Também não deixem de mandar sugestões para temas, por aqui ou me referenciando no twitter.

Advertisements

3 thoughts on “Access Control List: Protegendo o acesso a seus objetos

  1. Oi Alberto, muito bom!

    Acho que nunca precisei usar ACL do Spring Security, mas certamente é uma feature poderosa e ao mesmo tempo muito complexo. Quem disse que segurança é algo simples, não é?

    Creio que em muitas apps é possível evitar toda essa configuração e gerenciamento de ACL com código simples como aquele seu do inicio do post – na minha opinião fica até claro e fácil de testar. Outro detalhe importante seria usar UUID em vez de sequenciais nas URLs restritas, assim diminuiria substancialmente as chances de um usuário mal intencionado adivinhar as URLs e ter acesso indevido. O que acha?

    Um abraço!

    Like

  2. Opa Rafael,

    Fazer na mão é uma opção, com certeza. Acho que tudo vai depender quantas vezes você vai fazer isso, da quantidade de objetos relacionados e tudo mais… A configuração é chata, mas também acho que ela é chata só uma vez. E usar o UUID é uma opção bem justa, com certeza :).

    Abraço!

    Like

    • Pois é, uma das vantagens e desvantagens do Spring é justamente essa: fazer a configuração somente uma única vez, rss 🙂

      Por isso entender o processo e necessidade de cada bean, assim como você faz e explica, é o diferencial para dominar o framework.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s