Controle de acesso com o Demoiselle 2.1

09maio11

Controle de acesso fácil

Na semana passada foi anunciada a versão 2.1 do framework Demoiselle. Dentre as novidades está o controle de acesso simples, fácil e flexível. Neste post explico como personalizar esta funcionalidade mostrando código funcionando. Veja como é fácil!

Se você quer controlar o acesso à sua aplicação, saiba que a versão 2.1 do Demoiselle vai facilitar muito a sua vida. No exemplo, criei uma aplicação através do arquétipo demoiselle-minimal (veja neste vídeo como fazer isto). O primeiro passo foi definir os pontos de controle. Utilizei anotações:

public class HelloWorld {

	@Inject
	private Logger logger;

	@RequiredPermission(resource = "hello", operation = "say")
	public void say() {
		logger.info("Saying hello on console");
	}

	@RequiredRole("administrators")
	public void swear() {
		logger.info("I swear...");
	}
}

Para executar o método say() é preciso ter permissão ao recurso “hello” com a ação “say”, conforme anotação @RequiredPermission. Para executar swear(), o usuário deve pertencer ao grupo de administradores. Só utilize a anotação @RequiredRole caso sua aplicação possua grupos fixos, senão dê preferência a @RequiredPermission. Se quer aprofundar neste assunto, sugiro a leitura do padrão RBAC.

O passo seguinte foi criar o autenticador e autorizador personalizados. Lembra dos conceitos de controle de acesso? Autenticar é identificar o usuário, autorizar é determinar o que pode ser acessado. Comecei pelo autenticador. Criei um objeto para transportar as credenciais do usuário, neste caso o login e senha, mas poderia ser qualquer outra coisa:

@SessionScoped
public class MyCredentials implements Serializable {

	private String username;

	private String password;

	public void clear() {
		username = null;
		password = null;
	}

	// Getters and setters
}

Em seguida criei o autenticador, uma implementação da interface Authenticator do Demoiselle:

@Alternative
public class MyAuthenticator implements Authenticator {

	@Inject
	private MyCredentials credentials;

	@Override
	public boolean authenticate() {
		String usr = credentials.getUsername();
		String pwd = credentials.getPassword();

		boolean authenticated = false;

		if (usr.equals("admin") && pwd.equals("secret")) {
			authenticated = true;
		} else if (usr.equals("zyc") && pwd.equals("tcharam")) {
			authenticated = true;
		}

		return authenticated;
	}

	@Override
	public void unAuthenticate() {
		credentials.clear();
	}

	@Override
	public User getUser() {
		return new User() {

			@Override
			public String getId() {
				return credentials.getUsername();
			}

			@Override
			public void setAttribute(Object key, Object value) {
			}

			@Override
			public Object getAttribute(Object key) {
				return null;
			}
		};
	}
}

A implementação foi bem simples, mas poderia ler um XML, acessar banco de dados, conectar ao LDAP, consultar outro sistema, consumir serviços web ou delegar para frameworks especializados.

Ativei a estratégia de autenticação no /META-INF/beans.xml:

<beans>
	<alternatives>
		<class>examples.MyAuthenticator</class>
	</alternatives>
</beans>

Para testar o funcionamento, criei um caso de teste com JUnit:

@RunWith(DemoiselleRunner.class)
public class HelloWorldTest {

	@Inject
	private SecurityContext context;

	@Inject
	private MyCredentials credentials;

	@Test
	public void loginSuccessful() {
		// Acessando com meu usuário
		credentials.setUsername("zyc");
		credentials.setPassword("tcharam");
		context.login();
		assertEquals("zyc", context.getUser().getId());
		context.logout();

		// Acessando como admin
		credentials.setUsername("admin");
		credentials.setPassword("secret");
		context.login();
		assertEquals("admin", context.getUser().getId());
		context.logout();
	}

	@Test
	public void loginFailed() {
		// Tentando acessar com usuário inexistente
		credentials.setUsername("fake");
		credentials.setPassword("idontknow");
		context.login();
		assertNull(context.getUser());
	}
}

O método login() da interface SecurityContext delega a chamada para a estratégia de autenticação ativa. Todas as funcionalidades de segurança devem ser acessadas exclusivamente através de SecurityContext. Olha as funcionalidades quais são:

interface SecurityContext {

	void login();

	void logout() throws NotLoggedInException;

	boolean isLoggedIn();

	boolean hasPermission(String resource, String operation)
			throws NotLoggedInException;

	boolean hasRole(String role) throws NotLoggedInException;

	User getUser();
}

Voltando ao que interessa… criei o autorizador, uma implementação da interface Authorizator do Demoiselle:

@Alternative
public class MyAuthorizator implements Authorizator {

	@Inject
	private SecurityContext context;

	@Override
	public boolean hasRole(String role) {
		String usr = context.getUser().getId();
		boolean authorized = false;

		if (usr.equals("admin") &&
		    role.equals("administrators")) {
			authorized = true;
		}

		return authorized;
	}

	@Override
	public boolean hasPermission(Object res, String op) {
		String usr = context.getUser().getId();
		boolean authorized = false;

		if (usr.equals("zyc") &&
		    res.equals("hello") && op.equals("say")) {
			authorized = true;
		} else if (context.hasRole("administrators")) {
			authorized = true;
		}

		return authorized;
	}
}

O método hasRole() só retorna verdade caso “admin” esteja autenticado. O hasPersmission() implementa uma regra específica para “zyc” e permite que integrantes do grupo de administradores tenham passe-livre. Estas regras poderiam estar implementadas de forma mais complexa, acessando um banco de dados, consumindo serviços web ou delegando para frameworks especializados.

Ativei a estratégia de autorização no /META-INF/beans.xml:

<beans>
	<alternatives>
		<class>examples.MyAuthenticator</class>
		<class>examples.MyAuthorizator</class>
	</alternatives>
</beans>

Em seguida complementei o caso de teste:

@RunWith(DemoiselleRunner.class)
public class HelloWorldTest {

	@Inject
	private SecurityContext context;

	@Inject
	private MyCredentials credentials;

	@Inject
	private HelloWorld helloWorld;

	@Test
	public void loginSuccessful() {
		// Acessando com meu usuário
		credentials.setUsername("zyc");
		credentials.setPassword("tcharam");
		context.login();
		assertEquals("zyc", context.getUser().getId());
		context.logout();

		// Acessando como admin
		credentials.setUsername("admin");
		credentials.setPassword("secret");
		context.login();
		assertEquals("admin", context.getUser().getId());
		context.logout();
	}

	@Test
	public void loginFailed() {
		// Tentando acessar com usuário inexistente
		credentials.setUsername("fake");
		credentials.setPassword("idontknow");
		context.login();
		assertNull(context.getUser());
	}

	@Test
	public void authorizedAccess() {
		// Acessando o método "say" com meu usuário
		credentials.setUsername("zyc");
		credentials.setPassword("tcharam");
		context.login();
		helloWorld.say();
		context.logout();

		// Acessando o método "say" e "swear" como admin
		credentials.setUsername("admin");
		credentials.setPassword("secret");
		context.login();
		helloWorld.say();
		helloWorld.swear();
		context.logout();
	}

	@Test
	public void unauthorizedAccess() {
		// Tentando acessar o método "swear"
		credentials.setUsername("zyc");
		credentials.setPassword("tcharam");
		context.login();

		try {
			helloWorld.swear();
			fail();
		} catch (SecurityException e) {
			// Se chegar aqui, o teste deve passar
		}

		context.logout();
	}
}

Tudo pronto e funcionando perfeitamente! O código-fonte produzido pode ser acessado publicamente através deste repositório, sinta-se à vontade para baixar, experimentar e modificar. A documentação de referência do Demoiselle pode ser acessada aqui.

Para facilitar ainda mais sua vida, disponibilizamos uma extensão para o Apache Shiro e estamos trabalhando na integração com o projeto Rasea. Em breve, mais novidades!

Anúncios


7 Responses to “Controle de acesso com o Demoiselle 2.1”

  1. 1 Ewerton Henrique

    Fiquei com uma dúvida.

    O método login da implementação de securityContext é do tipo void.
    E quando eu chamo
    ele vai para index como faço para ir para página que eu desejar?

    //Metodo login do securityContext
    @Override
    public void login() {
    if (config.isEnabled() && authenticator.get().authenticate()) {
    Authenticator auth = authenticator.get();

    user = auth.getUser();
    loggedIn = true;

    // Esse código abaixo redireciona para a index.

    Beans.getBeanManager().fireEvent(new AfterLoginSuccessful() {

    private static final long serialVersionUID = 1L;

    });
    }
    }

  2. 3 ronaldogiusti

    Cleverson, excelente post!
    Tenho uma dúvida: esse mecanismo de autenticação/autorização é compatível com o método login do Servlet 3.0? Mais expecificamente, uma chamada a request.isUserInRole(“role”) funcionaria?

    um abraço.

  3. 4 Alex

    Cleverson,
    Segui seu post, fiz uma tela de login no meu projeto (jsf + jpa), porem quando dou submit apresenta a seguinte mensagem de erro, e mais nada: Erro de conversão ao definir o valor ‘alex’ para ‘null Converter’
    No console não imprime nada.
    O que pode ser?


  1. 1 Controle de acesso com o Demoiselle 2.1 « Demoiselle Framework's Blog
  2. 2 Demoiselle 2 + Rasea « Cleverson Sacramento

E aí, o que você achou? Comenta aí...

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s