Ajuda para lançar plugin de controle de acesso para VRaptor

Quero lançar um plugin de controle de acesso, mas queria saber algumas coisas antes, como padrões de nome, diretório, etc. E também opiniões e sugestões das pessoas :smiley:

Já tinha dado uma olhada em alguns posts do GUJ sobre plugins de controle de acesso pro VRaptor, mas acheis todos complicados e não atendiam todas as minhas necessidades, então decidi fazer uma mais simples.

Eu já estou usando o código em uma projeto meu, até onde sei está funcionando, mas ainda faltam testes unitários, testes de performance, essas coisas.

Minhas dúvidas
[list]Como lanço meu códgo como plugin, basta eu exportar para um jar e postar em algum lugar?[/list]
[list]Quais arquivos são necessários?[/list]
[list]Existe algum padrão de nomes? Por exemplo, estou chamando as classes de Resource e os métodos de métodos mesmo.[/list]

Resumo de como funciona

O controle de acesso é baseado em papéis (roles), então está bem genérico, podendo ser utilizado não só para logado/não-logado, mas para qualquer papel.

O plugin é basicamente um interceptor que lê as anotações dos Controllers e métodos anotados com @AllowAccessTo(“papel1, papel2, papel3”) e @DenyAccessTo(“papel4, papel5, papel 6”).

A princípio, todos os métodos e classes são livres para serem acessados. Caso a classe ou o método seja anotado, então vai ser controlado.
Se um método estiver anotado com o @AllowAccessTo(“loggedUser”), então somente usuário com esse papel poderão acessar o método.
Se um método estiver anotado com o @DenyAccessTo(“notLoggedUser”), então todos os usuários com um papel diferente poderá acessar aquele método.
Se uma classe Controller estiver anotada, essa regra vale para todos os métodos que não estejam anotados. Ou seja, mesmo que uma classe esteja anotada com @AllowAccessTo(“admin”), se o método estiver anotado com @AllowAccessTo(“client”), então esse método só poderá ser acessado pelo usuário com o papel “client”. Eu decidi que o método anotado tem prioridade sobre a classe anotada.

É possível adicionar mais de um papel para o método ou a classe. Só usar @AllowAccessTo(“client, admin”). Sendo assim, o usuário com o papel “client” ou com o papel “admin” poderá acessar.

A lógica de autenticação ainda tem que ser implementada pelo programador, pois não tem como o plugin saber quem é de uma papel e quem é do outro. Então o programador tem que injetar o @Component UserAccessDefinitions para poder adicionar os papeis ao usuário.

Por exemplo:

@Post
public void authenticate(User user){
    if(this.userDao.retrieveUserWith(user.getLogin(), user.getPassword()!=null){
        this.userAccessDefinitions.getRoles().add("loggedUser");
        // Mais coisas aqui
    }else{
        //Mais coisas aqui
    }
}

Ainda vai ter a opção de adicionar permissões especificando os métodos e as classes, e não por papel. Só chamar o método do @Component UserAccessDefinitions, que adicionando métodos e classes neles.

Por exemplo, se o usuário tentar entrar com SQLInjection em um dos campos, se adiciona o método que leva o usuário para a página de login para os métodos negados:

@Post
public void authenticate(User user){
    if(sqlInjectionIsPresentOn(user.getLogin()) || sqlInjectionIsPresentOn(user.getPassword()){
    try {
				this.userAccessDefinition.getMethodDenials().add(UserController.class.getMethod("login"));
			} catch (SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (NoSuchMethodException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
    }else{
        if(this.userDao.retrieveUserWith(user.getLogin(), user.getPassword()!=null){
            this.userAccessDefinitions.getRoles().add("loggedUser");
            // Mais coisas aqui
        }else{
            //Mais coisas aqui
        }
    }
}

Seguem as classes que eu criei para o plugin:

-Anotações

[code]/**
*
*/
package com.eAccessCon.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**

  • @author maiconfz

*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { ElementType.TYPE, ElementType.METHOD })
public @interface DenyAccessTo {
public String value();
}[/code]

[code]/**
*
*/
package com.eAccessCon.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**

  • @author maiconfz

*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = { ElementType.TYPE, ElementType.METHOD })
public @interface AllowAccessTo {
public String value();
}[/code]
-Classe de permissões da sessão do usuário (Serve para definir os papeis do usuário, permissões para métodos e Controllers específicos)

[code]package com.eAccessCon.session;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import br.com.caelum.vraptor.ioc.Component;
import br.com.caelum.vraptor.ioc.SessionScoped;

import com.eAccessCon.provider.EAccessConInitialUserAccessDefinitionProvider;

/**

  • @author maiconfz

*/
@Component
@SessionScoped
public class UserAccessDefinitions implements Serializable {

private static final long serialVersionUID = 5301380832545035596L;


private List<Method> methodDenials;
private List<Method> methodPermissions;
private List<Class<?>> resourceDenials;
private List<Class<?>> resourcePermissions;
private List<String> roles;

/**
 * @param valuesProvider
 */
public UserAccessDefinitions(
		EAccessConInitialUserAccessDefinitionProvider valuesProvider) {
	if (valuesProvider.getMethodDenials() != null) {
		this.methodDenials = valuesProvider.getMethodDenials();
	} else {
		this.methodDenials = new ArrayList<Method>();
	}

	if (valuesProvider.getMethodPermissions() != null) {
		this.methodPermissions = valuesProvider.getMethodPermissions();
	} else {
		this.methodPermissions = new ArrayList<Method>();
	}

	if (valuesProvider.getResourceDenials() != null) {
		this.resourceDenials = valuesProvider.getResourceDenials();
	} else {
		this.resourceDenials = new ArrayList<Class<?>>();
	}

	if (valuesProvider.getResourcePermissions() != null) {
		this.resourcePermissions = valuesProvider.getResourcePermissions();
	} else {
		this.resourcePermissions = new ArrayList<Class<?>>();
	}

	if (valuesProvider.getRoles() != null) {
		this.roles = valuesProvider.getRoles();
	} else {
		this.roles = new ArrayList<String>();
	}

}

    // GETTERS e SETTERS

}
[/code]
-Interceptor com a lógica de controle

[code]/**
*
*/
package com.eAccessCon.interceptor;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

import javax.servlet.http.HttpServletResponse;

import br.com.caelum.vraptor.InterceptionException;
import br.com.caelum.vraptor.Intercepts;
import br.com.caelum.vraptor.Result;
import br.com.caelum.vraptor.core.InterceptorStack;
import br.com.caelum.vraptor.interceptor.Interceptor;
import br.com.caelum.vraptor.resource.ResourceMethod;
import br.com.caelum.vraptor.view.Results;

import com.eAccessCon.annotation.AllowAccessTo;
import com.eAccessCon.annotation.DenyAccessTo;
import com.eAccessCon.session.UserAccessDefinitions;

/**

  • @author maiconfz

*/
@Intercepts
public class EAccessConInterceptor implements Interceptor {
private UserAccessDefinitions userAccessDefinitions;
private Result result;

/**
 * 
 * @param userAccessDefinitions
 * @param result
 */
public EAccessConInterceptor(UserAccessDefinitions userAccessDefinitions,
		Result result) {
	this.userAccessDefinitions = userAccessDefinitions;
	this.result = result;
}

/*
 * (non-Javadoc)
 * 
 * @see
 * br.com.caelum.vraptor.interceptor.Interceptor#accepts(br.com.caelum.vraptor
 * .resource.ResourceMethod)
 */
@Override
public boolean accepts(ResourceMethod arg0) {
	return true;
}

/*
 * (non-Javadoc)
 * 
 * @see
 * br.com.caelum.vraptor.interceptor.Interceptor#intercept(br.com.caelum
 * .vraptor.core.InterceptorStack,
 * br.com.caelum.vraptor.resource.ResourceMethod, java.lang.Object)
 */
@Override
public void intercept(InterceptorStack arg0, ResourceMethod arg1,
		Object arg2) throws InterceptionException {

	Method requestedMethod = arg1.getMethod();
	Class<?> requestedResource = arg1.getResource().getType();

	List<Method> userMethodDenials = this.userAccessDefinitions
			.getMethodDenials();
	List<Method> userMethodPermissions = this.userAccessDefinitions
			.getMethodPermissions();

	List<Class<?>> userResourceDenials = this.userAccessDefinitions
			.getResourceDenials();
	List<Class<?>> userResourcePermissions = this.userAccessDefinitions
			.getResourcePermissions();

	List<String> userRoles = this.userAccessDefinitions.getRoles();

	// By default, user is allowed to access
	Boolean userAllowed = true;

	if (!userMethodPermissions.isEmpty()
			&& !userMethodPermissions.contains(requestedMethod)) {
		userAllowed = false;
	} else if (!userMethodDenials.isEmpty()
			&& userMethodDenials.contains(requestedMethod)) {
		userAllowed = false;
	} else if (!userResourcePermissions.isEmpty()
			&& !userResourcePermissions.contains(requestedResource)) {
		userAllowed = false;
	} else if (!userResourceDenials.isEmpty()
			&& userResourceDenials.contains(requestedResource)) {
		userAllowed = false;
	} else if (requestedMethod.isAnnotationPresent(AllowAccessTo.class)
			|| requestedMethod.isAnnotationPresent(DenyAccessTo.class)
			|| requestedResource.isAnnotationPresent(AllowAccessTo.class)
			|| requestedResource.isAnnotationPresent(DenyAccessTo.class)) {

		String annotationRoles;
		List<String> annotationRolesList;
		Boolean methodOrClassIsAnnotatedWithAllowAccessTo;
		Boolean roleMatch = false;

		// Set the method or class roles to variable annotationRoles
		if (requestedMethod.isAnnotationPresent(AllowAccessTo.class)) {
			annotationRoles = requestedMethod.getAnnotation(
					AllowAccessTo.class).value();
			methodOrClassIsAnnotatedWithAllowAccessTo = true;
		} else if (requestedMethod.isAnnotationPresent(DenyAccessTo.class)) {
			annotationRoles = requestedMethod.getAnnotation(
					DenyAccessTo.class).value();
			methodOrClassIsAnnotatedWithAllowAccessTo = false;
		} else if (requestedResource
				.isAnnotationPresent(AllowAccessTo.class)) {
			annotationRoles = requestedResource.getAnnotation(
					AllowAccessTo.class).value();
			methodOrClassIsAnnotatedWithAllowAccessTo = true;
		} else {
			// It only reachs here if Class is annotated with @DenyAccessTo
			annotationRoles = requestedResource.getAnnotation(
					DenyAccessTo.class).value();
			methodOrClassIsAnnotatedWithAllowAccessTo = false;
		}

		// By default, the method or resource roles are merged
		// ("role1, role2, role3"), so it removes white spaces and splits
		// the roles
		annotationRolesList = Arrays.asList(annotationRoles.replaceAll(" ",
				"").split(","));

		if (methodOrClassIsAnnotatedWithAllowAccessTo) {
			for (String role : annotationRolesList) {
				if (userRoles.contains(role)) {
					roleMatch = true;
					break;
				}
			}

			if (!roleMatch) {
				userAllowed = false;
			}

		} else {
			// if method or class is annotated with DenyAccessTo
			for (String role : annotationRolesList) {
				if (userRoles.contains(role)) {
					roleMatch = true;
					break;
				}
			}

			if (roleMatch) {
				userAllowed = false;
			}
		}
	} else {
		// If class or method is not annotated with AllowAccessTo neither
		// DenyAccessTo, then user is allowed to access
		userAllowed = true;
	}

	if (userAllowed) {
		arg0.next(arg1, arg2);
	} else {
		this.result.use(Results.http()).sendError(
				HttpServletResponse.SC_FORBIDDEN);
	}

}

}
[/code]
- Interface e Classe padrão para definir papeis e permissões no início da sessão do usuário(Essa parte está meio gambiarrada ainda, estou procurando soluções.

package com.eAccessCon.provider;

import java.lang.reflect.Method;
import java.util.List;

/**
 * @author maiconfz
 * 
 */
public interface EAccessConInitialUserAccessDefinitionProvider {

	public List<String> getRoles();

	public List<Method> getMethodDenials();

	public List<Method> getMethodPermissions();

	public List<Class<?>> getResourceDenials();

	public List<Class<?>> getResourcePermissions();

}

[code]package com.eAccessCon.provider;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**

  • @author maiconfz

*/
public class DefaulEAccessInitialDefaultValueProvider implements
EAccessConInitialUserAccessDefinitionProvider {

/*
 * (non-Javadoc)
 * 
 * @see
 * com.eAccessCon.provider.EAccessConInitialUserAccessDefinitionProvider
 * #getRoles()
 */
@Override
public List<String> getRoles() {
	return new ArrayList<String>();
}

/*
 * (non-Javadoc)
 * 
 * @see
 * com.eAccessCon.provider.EAccessConInitialUserAccessDefinitionProvider
 * #getMethodDenials()
 */
@Override
public List<Method> getMethodDenials() {
	return new ArrayList<Method>();
}

/*
 * (non-Javadoc)
 * 
 * @see
 * com.eAccessCon.provider.EAccessConInitialUserAccessDefinitionProvider
 * #getMethodPermissions()
 */
@Override
public List<Method> getMethodPermissions() {
	return new ArrayList<Method>();
}

/*
 * (non-Javadoc)
 * 
 * @see
 * com.eAccessCon.provider.EAccessConInitialUserAccessDefinitionProvider
 * #getResourceDenials()
 */
@Override
public List<Class<?>> getResourceDenials() {
	return new ArrayList<Class<?>>();
}

/*
 * (non-Javadoc)
 * 
 * @see
 * com.eAccessCon.provider.EAccessConInitialUserAccessDefinitionProvider
 * #getResourcePermissions()
 */
@Override
public List<Class<?>> getResourcePermissions() {
	return new ArrayList<Class<?>>();
}

}
[/code]

-Exemplo de Controller com as anotações (O Controller não faz parte do plugin, isso deve ser feito por quem vai usá-lo

/**
 * 
 */
package com.exemplo.vraptor.controller;

import br.com.caelum.vraptor.Get;
import br.com.caelum.vraptor.Post;
import br.com.caelum.vraptor.Resource;

import com.eAccessCon.annotation.AllowAccessTo;
import com.eAccessCon.session.UserAccessDefinitions;

/**
 * @author maiconfz
 * 
 */
@Resource
public class UserController {
	private UserAccessDefinitions userAccessDefinitions;

	/**
	 * @param userAccessDefinitions
	 */
	public UserController(UserAccessDefinitions userAccessDefinitions) {w
		this.userAccessDefinitions = userAccessDefinitions;
	}

	/**
	 * 
	 */
	@Get
	@AllowAccessTo("NotAuthenticatedUser")
	public void login() {
		// Lógica
	}

	/**
	 * 
	 */
	@Post
	@AllowAccessTo("NotAuthenticatedUser")
	public void authenticate(
			AuthenticationRequestInput authenticationRequestInput) {
		// Lógica de autenticação
                // É aqui que, normalmente, você vai definir o papel para o usuário.
                // Chamando o método this.userAccessDefinitions.getRoles().add("AuthenticatedUser");
	}

	@Get
	@AllowAccessTo("AuthenticatedUser")
	public void home() {
		// lógica
	}

	@Get
	@AllowAccessTo("NotAuthenticatedUser")
	public void signup() {
		//Lógica
	}

	@Post
	@AllowAccessTo("NotAuthenticatedUser")
	public void register(
			UserRegistrationRequestInput userRegistrationRequestInput) {
                // Lógica
        }
}

– Caso queira definir valores padrões para os papeis e permissões do usuário, o programador deve implementar a interface EAccessConInitialUserAccessDefinitionProvider, mencionada anteriormente. Então definir um customProvider para o VRaptor e registrar a sua implementação para a minha interface. (Eu sei, eu sei, ficou um pouco complicado. Estou procurando soluções para isso.)

@Component
@ApplicationScoped
public class MyEAccessConInitialUserAccessDefinitionProvider implements
		EAccessConInitialUserAccessDefinitionProvider {

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eAccessCon.provider.
	 * EAccessConInitialUserAccessDefinitionProvider#getRoles()
	 */
	@Override
	public List<String> getRoles() {
		List<String> roles = new ArrayList<String>();
		roles.add("NotAuthenticatedUser");
		return roles;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eAccessCon.provider.
	 * EAccessConInitialUserAccessDefinitionProvider#getMethodDenials()
	 */
	@Override
	public List<Method> getMethodDenials() {
		// TODO Auto-generated method stub
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eAccessCon.provider.
	 * EAccessConInitialUserAccessDefinitionProvider#getMethodPermissions()
	 */
	@Override
	public List<Method> getMethodPermissions() {
		// TODO Auto-generated method stub
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eAccessCon.provider.
	 * EAccessConInitialUserAccessDefinitionProvider#getResorceDenials()
	 */
	@Override
	public List<Class<?>> getResourceDenials() {
		// TODO Auto-generated method stub
		return null;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see com.eAccessCon.provider.
	 * EAccessConInitialUserAccessDefinitionProvider#getResourcePermissions()
	 */
	@Override
	public List<Class<?>> getResourcePermissions() {
		// TODO Auto-generated method stub
		return null;
	}

}

public class CustomProvider extends SpringProvider { /* * (non-Javadoc) * * @see * br.com.caelum.vraptor.ioc.spring.SpringProvider#registerCustomComponents * (br.com.caelum.vraptor.ComponentRegistry) */ @Override protected void registerCustomComponents(ComponentRegistry registry) { registry.register(EAccessConInitialUserAccessDefinitionProvider.class, MyEAccessConInitialUserAccessDefinitionProvider.class); super.registerCustomComponents(registry); } }
Colocar no web.xml

<context-param>
		<param-name>br.com.caelum.vraptor.provider</param-name>
		<param-value>com.exemplo.vraptor.custom.CustomProvider</param-value>
</context-param>

Eu gostaria de sugerir que as anotações @AllowAccessTo e @DenyAccessTo recebessem um array de Enum no value()…
Assim evitaria da pessoa colocar uma string toda bagunçada… Por exemplo: “Arroz” != “arroz”.

Também gostaria de sugerir um interceptor que colocasse variáveis booleans na view para que fossem usadas no <c:if />
Por exemplo: o meu usuário tem o perfilA, logo, cria na view a Boolean perfilA com o valor true.

Quando eu fizesse <c:if test=${perfilA}>Link do PerfilA</c:if> o link só apareceria para quem tem o perfilA.

Eu acho que é um atalho legal e evita de ficar procurando se o usuário possui aquele perfil.

Você poderia considerar também o fato de um usuário ter mais de um perfil.

Para compartilhar o plugin, tem o github, você cria o jar e posta la.

Existe uma forma de você fazer com que o jar seja reconhecido automaticamente pelo VRaptor, sem a necessidade de configurar o web.xml:

Dentro do seu projeto do plugin, crie a pasta META-INF se ela não existir e dentro dela crie um arquivo chamado “br.com.caelum.vraptor.packages” e dentro dele você coloca o seu package pai: “br.com.plugin.do.maiconfz”, ai você exporta para um jar e só coloca ele na pasta lib do seu projeto, sem configurar nada.

Existe também o seguinte problema: Eu uso Guice como container do VRaptor, nesse caso o seu plugin iria falhar, pois ele está usando o Spring. Acho que você deveria ver um jeito de fazer ele funcionar nos 3 conteiners…

Sobre usar o Enum: Ficaria mais fácil de não errar, mais aí a pessoa teria que ficar criando enum, e não só anotar com string. E também, não achei nada de como criar Enums em tempo de execução, pois mais tarde eu quero permitir que adicione papeis em tempo de execução aos métodos e classes.

Sobre a parte do Container eu acho que eu já resolvi. Eu tava usando o CustomProvider, pq não sabia que dava para receber uma lista de todas as implementações da minha interface.

Sobre o perfil na sessão, achei uma boa ideia. Vou pensar em como implementar.

para publicar um plugin, coloque o código do projeto no github.com

vá em https://github.com/caelum/vraptor-contrib, adicione seu projeto lá e mande um pull request.

coloque um jeito fácil de gerar o jar do projeto, e/ou coloque o jar no projeto do github (tem uma parte de downloads lá)

Acho que o campeão de plugins para VRaptor é o controle de acesso.

Tem pelo menos uns 5 plugins, cada um para um sabor diferente. E para aqueles que, como eu, preferem usar a API padrão, JAAS.

Parabéns pelo plugin. Não esquece de publicar no vraptor-contrib (como o Lucas já citou) e escrever algum doc resumido no readme.

[quote=garcia-jj]Acho que o campeão de plugins para VRaptor é o controle de acesso.

Tem pelo menos uns 5 plugins, cada um para um sabor diferente. E para aqueles que, como eu, preferem usar a API padrão, JAAS.

Parabéns pelo plugin. Não esquece de publicar no vraptor-contrib (como o Lucas já citou) e escrever algum doc resumido no readme.[/quote]

Eu percebi quando procurei por um. O problema é que eu precisava de algo mais específico. Aí como o código já tava pronto, pq não publicar pros outros? hehehe.

Sobre o JAAS, nem sabia o que era. Vou tentar procurar e aprender.