Durante o desenvolvimento de aplicações corporativas em geral, nos deparamos sempre com requisitos de registro de histórico ou auditoria em banco de dados. Para isso podemos utilizar a ferramenta Hibernate Envers que foi iniciado como um projeto do JBoss e futuramente será englobado pelo Hibernate (provavelmente na versão 3.5, conforme anunciado no site do projeto).
Realizamos uma prova de conceito desta ferramenta visando assegurar o seu uso em nossos projetos. Para a prova de conceito utilizamos a versão 1.2.0.GA, que funciona somente com o Hibernate 3.3. Por este motivo, não foi possível utilizá-lo no JBoss 4.2.3 (que utiliza o Hibernate 3.2) e não foi trivialmente possível substituir a versão do Hibernate no JBoss para fazer o Envers funcionar.
A prova de conceito foi desenvolvida utilizando o myContainer e construindo uma suite de testes unitários para verificar se o framework funciona para o que se propõe a fazer. Posteriormente o módulo EJB do projeto foi deployado no JBoss AS 5.0.1.GA, onde os testes foram executados utilizando um cliente remoto.
O framework foi avaliado em vários aspectos, que serão explicados a seguir.
Configuração
Adicionar auditoria em um projeto é muito simples. Basta anotar as entidades em que se deseja auditoria e fazer uma simples configuração no arquivo persistence.xml. Para cada entidade anotada com @Audited (org.hibernate.envers.Audited) o framework cria uma nova tabela do banco. Por exemplo, se anotarmos e entidade Cliente com @Audited, o framework irá criar uma tabela Cliente_AUD onde irá replicar as informações da entidade modificada e registrar alguns dados como número de revisão e operação (remoção/alteração/inclusão). É possível também utilizar um controle mais granular, indicando quais campos de cada entidade devem ser auditados e definir o nome das tabelas/colunas geradas pelo framework.
O framework utiliza o mesmo conceito do Subversion com relação às revisões, ou seja, cada revisão é um marco global possuindo um número que é unico para o banco de dados inteiro e não um número por tabela ou registro. As alterações que forem feitas em uma mesma transação irão possuir o mesmo número de revisão.
A configuração é bastante simples e para ativar o Envers basta adicionar as seguintes linhas no persistence.xml:
<property name="hibernate.ejb.event.post-insert" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-update" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-delete" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-update" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.pre-collection-remove" value="org.hibernate.envers.event.AuditEventListener" /> <property name="hibernate.ejb.event.post-collection-recreate" value="org.hibernate.envers.event.AuditEventListener" />
Volume de Dados Gerado
Uma das grandes preocupações quando pensamos em histórico de registros é o volume de dados gerado e a performance da aplicação. Este problema foi levado em consideração pelo framework pois o mecanismo de replicação de registros e relacionamentos é bastante inteligente. Todos os relacionamentos de uma entidade auditada precisam, obrigatoriamente, ser entidades auditadas, porém o mecanismo cria registros de histórico somente quando necessário. Por exemplo: imagine que temos o relacionamento “Cliente 1—>* Contato” (um cliente possui vários contatos). Ao alterar um cliente, o framework cria um registro de histórico somente para o registro alterado. Será criado registro de histórico para os contatos somente se eles forem também alterados. Poderíamos até mesmo adicionar um novo contato na lista que o framework irá criar somente o histórico do cliente e do novo contato adicionado, não criando registros de histórico para os demais contatos associados ao cliente em questão. Desta forma, o volume de dados gerado é considerado aceitável.
Consulta de Histórico
O framework fornece duas APIs para consulta de histórico:
query muito semelhante à Criteria API do Hibernate. Pode ser utilizado para fazer consultas mais complexas.Ponto fraco: Ao consultar o histórico de uma entidade, todos os seus relacionamentos comportam-se como se o FetchType fosse Lazy. Outro problema é que não é possível resolver estas dependências posteriormente (em outra sessão do Hibernate), sendo necessário carregar todos os relacionamentos que serão utilizados no momento da consulta.
BUG: Ao consultar um número de revisão inexistente com a API AuditReader é retornado o registro de histórico mais atual. Deveria retornar null segundo a documentação.
Ponto forte: Navegação entre os registros de histórico: Após consultar um registro de histórico em uma determinada revisão com o AuditReader é possível navegar por todo o grafo de relacionamentos deste registro no estado em que ele estava no número da revisão utilizado para pesquisa.
Customização dos Registros de Histórico
O registro padrão de histórico armazena somente o número da revisão e a data da alteração do registro (em uma tabela separada chamada REVINFO).
Em muitos casos é necessário customizar o registro de histórico para armazenar mais informações como, por exemplo, o usuário que efetuou a alteração. O framework possui uma maneira simples de fazer essa customização. Basta criar uma nova entidade e utilizar a anotação @RevisionEntity. Essa anotação define que a entidade é uma “Entidade de Revisão”, ou seja, ela é uma entidade separada que mantém informações relacionadas a um número de revisão. Todas as tabelas de histórico possuem uma FK que aponta para essa tabela e ela substituirá a tabela REVINFO.
Para preencher essa entidade com as informações necessárias (por exemplo: o login do usuário), é necessário criar uma classe que implemente a interface RevisionListener e passar essa classe como parâmetro da anotação @RevisionEntity. O método newRevision dessa classe será chamado sempre que um novo registro de histórico estiver sendo criado.
Consulta de histórico diretamente no Banco de dados
O framework gera o banco de dados de uma maneira que as tabelas de histórico possuem a mesma estrutura das tabelas reais, inclusive as tabelas de relacionamentos. Desta maneira é possível fazer consultas no banco de histórico como se fosse o banco real, em um determinado marco no tempo, definido por uma data ou número de revisão, apenas fazendo join com a tabela REVINFO (ou outra tabela caso o registro de histórico seja customizado).
Resumo
Em resumo podemos citar como pontos positivos:
Pontos negativos:
fetch dos relacionamentos e tratar tudo como lazy.JPA.Prova de Conceito
Ao executar, os dois testes deverão acusar falhas. Estas falhas foram mantidas nos casos de teste porque, segundo a documentação do Hibernate Envers, o comportamento esperado é diferente do comportamento obtido ou desejado.
Clique aqui para fazer o download dos casos de teste implementados na prova de conceito.
Share on Facebook
A segurança de dados é um assunto que vem se tornando cada vez mais relevante no desenvolvimento de aplicações corporativas. Em geral, podemos proteger com relativa facilidade as funcionalidades de nossos sistemas, atribuindo permissões de acesso aos usuários e verificando estas permissões durante a sua execução. Mas isto não é suficiente em aplicações corporativas. Por exemplo, em um sistema acadêmico, somente um professor pode atribuir notas a um aluno, correto? Isso parece fácil de ser implementado. No entanto, pode um professor atribuir uma nota a qualquer aluno? Definitivamente não! Ou seja, precisamos garantir a segurança dos dados, que poderia ser definida como: “A capacidade de um sistema de garantir que as informações fiquem restritas a um contexto de negócio”. Traduzindo, um usuário não pode visualizar ou alterar dados que não estejam em seu contexto de negócio, e não somente em suas permissões funcionais.
Este objetivo é bastante ambicioso, mas totalmente necessário, e, em termos tecnológicos, temos evoluído bastante neste sentido. A especificação JavaEE 5 simplesmente ignora este problema, mas existem algumas soluções de mercado bastante robustas. Irei discorrer sobre duas delas (Spring Security e JBoss Seam), descrever rapidamente a solução que adotamos na Dextra para o desenvolvimento de um sistema de gestão acadêmica e, finalmente, propor uma nova e diferenciada abordagem.
1 – Spring Security
O Spring Security tem como objetivo garantir a segurança no acesso aos objetos de domínio de uma aplicação. Através deste mecanismo, o componente de segurança impede que um objeto de domínio específico seja carregado ou alterado de acordo com uma ACL (access control list) cadastrada em banco de dados. Basicamente, ele permite que sejam armazenados e obtidos de forma eficiente uma ACL para cada um dos objetos de domínio de sua aplicação. Esta abordagem não me parece muito interessante porque, em geral, não quero cadastrar uma ACL para cada um dos meus objetos de domínio. Não quero, por exemplo, cadastrar quais professores podem atribuir notas para cada aluno do meu sistema. E, afinal, meus requisitos são um pouco mais complexos e difíceis de serem implementados desta forma, pois não quero que um professor possa atribuir uma nota em qualquer matéria para um aluno, mas somente em uma matéria e turma específicas. Ou seja, minha regra de segurança pode envolver um ou mais objetos de domínio, relacionados entre si. Com o Spring Security seria preciso, por exemplo, sempre que um relacionamento entre aluno, turma, matéria, professor e outros for atualizado, reconstruir todas as ACLs afetadas.
2 – JBoss Seam
O JBoss Seam provê um mecanismo de segurança para objetos de domínio que utiliza uma abordagem diferenciada, baseada em regras. Utilizando a ferramenta Drools, é possível escrever regras que são eficientemente processadas e que permitem liberar ou bloquear o acesso do usuário a um objeto de domínio específico. Com o uso de regras no lugar de uma ACL, o mecanismo é muito mais abrangente e extensível, pois não preciso cadastrar as permissões em banco de dados, somente escrever uma regra de segurança que diz que um professor somente pode atribuir uma nota a um aluno quando este for professor de uma turma da qual o aluno faça parte. Há, no entanto, um forte acoplamento entre a solução do JBoss Seam e a necessidade de uso do framework JBoss Seam, que nem sempre é uma opção fácil de ser adotada.
3 – Usando aspectos
Recentemente, quando confrontado com este problema, adotei uma solução diferente das duas abordagens acima. Neste caso, a solução adotada envolveu o uso de aspectos através da ferramenta JBoss AOP. Com os aspectos em mãos, implementamos as regras de segurança que são checadas sempre que determinadas funcionalidades são executadas. Tomamos ainda uma decisão, no mínimo, controversa com relação ao uso deste mecanismo de segurança. O objetivo de todo mecanismo de segurança é garantir que o sistema funcione de forma segura. Para isso, basicamente, é preciso que o sistema funcione. Ou seja, a aplicação precisa implementar as mesmas regras definidas no mecanismo de segurança, senão certamente o mecanismo de segurança bloqueará o uso da sua aplicação. Como, neste caso específico, as regras de segurança eram muito complexas e tinham grande propensão a mudanças, optamos por implementar as regras de segurança somente no mecanismo de segurança. Desta forma, por exemplo, quando o mecanismo de segurança encontra um objeto de domínio em uma lista e descobre que ele não deveria estar ali, em vez de acusar uma falha de segurança, ele simplesmente remove o objeto da lista, mecanismo que nós denominamos de filtragem de segurança Assim, não foi preciso implementar em toda a aplicação as regras de segurança e, mesmo assim, o sistema garante segurança no acesso aos dados na aplicação.
Obs: É claro que existem alguns desafios de performance relacionados à solução adotada, mas tratamos performance caso-a-caso, como deve ser efetivamente tratado.
4 – Segurança de Dados – Responsabilidade dos próprios dados!
Apesar de todas as abordagens acima ilustradas, fico sempre ruminando a idéia de que a segurança no acesso a dados é uma característica intrínseca aos dados, não à aplicação. A aplicação deve se preocupar com suas funcionalidades, enquanto o banco de dados deveria se preocupar com os dados. Basicamente, nesta idéia “mirabolante”, seria responsabilidade do banco de dados criar uma visão dos dados para a aplicação. Ou seja, quando a aplicação acessa o banco de dados com um determinado usuário ou uma determinada visão, ela deveria ver somente os dados aos quais possui acesso, como se estes fossem os únicos dados existentes. É claro que esta não é uma solução simples, mas acredito que teremos que pensar um pouco sobre ela na evolução dos SGBDs atuais e no seu relacionamento com as aplicações.
De qualquer forma, continuamos evoluindo a segurança de nossas aplicações de forma rápida e responsável, adotando tecnologias não intrusivas e mecanismos cada vez mais performáticos e consistentes.
Share on FacebookMuito tem sido escrito sobre testes automáticos e testabilidade de software nos dias atuais. O rápido avanço tecnológico tem facilitado o desenvolvimento de testes unitários, de integração e funcionais. No entanto, o uso efetivo de testes automatizados não tem se mostrado constante nos sistemas. Na Dextra temos feito um grande esforço no sentido de usar metodologias e tecnologias de desenvolvimento que possibilitem a criação de testes automatizados em seus diversos níveis e temos entregado software com grande qualidade de testes automáticos. Mas, para isso, partimos da seguinte definição do que é “software bem testado”:
Pode-se dizer que um sistema é bem testado quando:
Esse objetivo é ambicioso pois deve garantir que:
Para alcançar estes objetivos, é necessário adotar duas diferentes e complementares abordagens: Arquitetura e Processo. Uma arquitetura bem definida pode garantir que um sistema seja testável, mas não garante que ele seja efetivamente testado. Apesar do advento de novas metodologias de desenvolvimento ágil, o desenvolvedor júnior (que é o mais abundante no mercado atualmente) não cria testes voluntariamente e nem cria testes bem feitos. É neste ponto que entra um processo de desenvolvimento que garanta a codificação de testes em uma cobertura racional do sistema com o envolvimento garantido de desenvolvedores experientes na sua elaboração e revisão. Neste sentido, infelizmente, a maioria dos processos baseados em metodologias tradicionais tem falhado. E tem falhado principalmente ao definir uma porcentagem mínima de cobertura de testes, em geral denominado “taxa de cobertura”. Infelizmente este número não tem significado algum que nos permita deduzir se um software é ou não bem testado. Seguem abaixo cinco razões para evitar este tipo de solução:
Basicamente o erro está em acreditar que a cobertura do código-fonte é importante, mas não é. O importante é que existam testes onde há maior probabilidade de falha. Testes de código que nunca falham não são verdadeiramente úteis. No entanto, testar somente o que vai falhar parece paradoxalmente complexo. Testar tudo poderia ser uma abordagem para testar sempre o que vai falhar, porém esta abordagem é dispendiosa e tem efeitos colaterais indesejados. Para resolver este problema, um processo deveria, pelo menos:
Bom, em um processo que possibilite a realização destas atividades, possuir a taxa de cobertura pode não agregar muito valor, mas não retira valor do que foi realizado. O que temos visto nos projetos da Dextra é que, através destas simples diretivas, uma alta taxa de cobertura acaba sendo atingida e com grande qualidade.
Share on Facebook