Páginas

terça-feira, 31 de dezembro de 2013

Log de auditoria com o Entity Framework 6

Uma das novidades no Entity Framework 6 é a possibilidade de interceptar os comandos SQL que são gerados pela ferramenta de forma bem mais simples que nas versões anteriores. Isso permite, entre outras coisas, efetuar um log de tudo o que é enviado para o banco de dados, facilitando a depuração das aplicações.

Mas hoje vou mostrar uma outra coisa bem interessante que podemos fazer ao interceptar o SQL do Entity Framework: log de auditoria. Ou seja, ter a rastreabilidade do que cada usuário fez no sistema, em determinado momento. A ideia central aqui é, para cada atualização de um registro no banco de dados (seja uma inserção, alteração ou exclusão), armazenar essa versão junto do usuário e data e hora de modificação.

Vamos olhar a tabela da imagem abaixo, People (à esquerda). Ela possui as colunas Id, FirstName, LastName e Age. Até aí, nada de mais. Agora vamos ver uma outra tabela de mesmo nome, só que criada em um schema diferente (hist). Ela possui as mesmas colunas, com os mesmos tipos, só que temos mais três colunas adicionais: Operation, para armazenar o tipo de operação que o registro recebeu (inserção, alteração e exclusão), Date, para registrar a data e hora da modificação, e User, para conter o login do usuário que efetuou a operação.

Para entender melhor, vamos dar uma olhada no exemplo de dados que essas tabelas possuem.


A tabela People do schema hist possui todas as versões de registros da tabela People. Com isso, podemos ver que no dia 20/11/2013, o usuário fabiogouw incluiu uma pessoa chamada "Celes Chere". Em 26/11/2013, o usuário matheussolino alterou a idade dessa pessoa para 17 anos. Então, o usuário fabiogouw excluiu essa pessoa em 01/12/2013. Apesar dessa pessoa não estar mais na tabela People, temos todo o rastreamento da sua existência no sistema. Com isso, podemos identificar o responsável por uma ação no sistema (imagine um sistema que calculou errado a folha de pagamentos de uma empresa; com o log de auditoria, podemos identificar quem foi o usuário responsável pelo erro).

Esse tipo de log não é nenhuma novidade. Há muito tempo essa estratégia é usada com o uso de triggers, por exemplo. Entretanto, iremos usar um comando novo nas últimas versões do SQL Server, o OUTPUT. Este comando direciona o resultado dos comandos de INSERT, UPDATE e DELETE (os registros afetados) de forma que podemos tanto enviá-los para a aplicação, como se fosse um comando SELECT, ou mesmo reutilizar esse retorno para inserir novamente no banco de dados. E é essa última forma que iremos utilizar neste exemplo.

Quando temos um comando de UPDATE, como abaixo, a ideia é inserir a cláusula de OUTPUT no meio do comando SQL.

Antes:
UPDATE [dbo].[People]

SET [Age] = @0

WHERE ([Id] = @1)

Depois:
UPDATE [dbo].[People]

SET [Age] = @0

 OUTPUT inserted.*, @histOperation, @histDate, @histUser into [hist].People

WHERE ([Id] = @1)

A cláusula OUTPUT redireciona o resultado dos registros atualizados (tabela virtual inserted) para dentro da tabela [hist].People. Justo desses registros também são passados os dados de auditoria, usuário, operação e data e hora, como parâmetros do comando.

A versão 6 do Entity Framework trouxe a classe DbInterception, que permite incluir classes que interceptam os comandos gerados por essa ferramenta. Essas classes devem implementar ou a interface IDbCommandInterceptor ou a interface IDbCommandTreeInterceptor. A primeira nos permite manipular o objeto DbCommand gerado pelo Entity Framework. A segunda permite manipular o objeto que armazena os dados do comando que será gerado (a árvore de expressões que será futuramente transformada em um comando SQL). Hoje iremos criar uma classe a partir de IDbCommandInterceptor, para que possamos alterar o objeto DbCommand, inserindo a cláusula OUTPUT e passando os parâmetros adicionais necessários.

using System;
using System.Data;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Linq;
using System.Text.RegularExpressions;

namespace EF6AuditInterception
{
    public class AuditInterceptor : IDbCommandInterceptor
    {
        private delegate DbCommand ChangeCommandDelegate(DbCommand command, string tableName);

        private string[] _tables;

        public AuditInterceptor(params string[] tables)
        {
            _tables = tables;
        }

        public void NonQueryExecuted(System.Data.Common.DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            //throw new NotImplementedException();
        }

        public void NonQueryExecuting(System.Data.Common.DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            command = HandleAuditInjection(command);
        }

        public void ReaderExecuted(System.Data.Common.DbCommand command, DbCommandInterceptionContext<System.Data.Common.DbDataReader> interceptionContext)
        {
            //throw new NotImplementedException();
        }

        public void ReaderExecuting(System.Data.Common.DbCommand command, DbCommandInterceptionContext<System.Data.Common.DbDataReader> interceptionContext)
        {
            command = HandleAuditInjection(command);
        }

        public void ScalarExecuted(System.Data.Common.DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            //throw new NotImplementedException();
        }

        public void ScalarExecuting(System.Data.Common.DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            //throw new NotImplementedException();
        }

        private DbCommand HandleAuditInjection(System.Data.Common.DbCommand command)
        {
            string tableName = GetTableName(command);
            if (_tables.Contains(tableName))
            {
                var del = GetChangeCommandDelegate(command);
                command = del(command, tableName);
            }
            return command;
        }

        private ChangeCommandDelegate GetChangeCommandDelegate(DbCommand command)
        {
            string commandText = command.CommandText;
            if (commandText.StartsWith("INSERT"))
                return ChangeInsertCommand;
            if (commandText.StartsWith("UPDATE"))
                return ChangeUpdateCommand;
            if (commandText.StartsWith("DELETE"))
                return ChangeDeleteCommand;
            return ChangeCommandWithDefault;
        }

        private DbCommand ChangeCommandWithDefault(DbCommand command, string tableName)
        {
            return command;
        }

        private DbCommand ChangeUpdateCommand(DbCommand command, string tableName)
        {
            int indexOfWhere = command.CommandText.IndexOf("WHERE");
            command = ReformatCommandText(command, indexOfWhere, "U", tableName);
            return command;
        }

        private DbCommand ChangeInsertCommand(DbCommand command, string tableName)
        {
            int indexOfWhere = command.CommandText.IndexOf("VALUES");
            command = ReformatCommandText(command, indexOfWhere, "I", tableName);
            return command;
        }

        private DbCommand ChangeDeleteCommand(DbCommand command, string tableName)
        {
            int indexOfWhere = command.CommandText.IndexOf("WHERE");
            command = ReformatCommandText(command, indexOfWhere, "D", tableName);
            return command;
        }

        private DbCommand ReformatCommandText(DbCommand command, int insertionIndex, string operation, string tableName)
        {
            string tableVersion = operation == "D" ? "deleted" : "inserted";
            command.CommandText = command.CommandText.Insert(insertionIndex,
                string.Format(" output {0}.*, @histOperation, @histDate, @histUser into [hist].{1} ", tableVersion, tableName));
            command = AddParameter(command, "@histOperation", DbType.String, operation);
            command = AddParameter(command, "@histDate", DbType.DateTime, DateTime.Now);
            command = AddParameter(command, "@histUser", DbType.String, Environment.UserName);
            return command;
        }

        private static Regex _tableNameRegex = new Regex(@"\[(.*?)\]");

        private string GetTableName(DbCommand command)
        {
            var matches = _tableNameRegex.Matches(command.CommandText);
            return matches.Count >= 2 ? matches[1].Groups[1].Value : string.Empty;
        }

        private DbCommand AddParameter(DbCommand command, string parameterName, DbType dbType, object value)
        {
            var param = command.CreateParameter();
            param.ParameterName = parameterName;
            param.DbType = dbType;
            param.Value = value;
            command.Parameters.Add(param);
            return command;
        }
    }
}

A interface IDbCommandInterceptor possui vários métodos onde podemos alterar o conteúdo do DbCommand, seja antes dele ser executado ou depois. Os métodos que terminam com Executing permitem interceptar o objeto DbCommand antes da sua execução, o que nos deixa alterá-lo. Já os métodos que terminam com Executed rodam após a execução do comando SQL. Com isso, podemos alterar o retorno que os componentes receberão, implementar cache, etc.

No código de exemplo, vejam os métodos NonQueryExecuting e ReaderExecuting. Eles são os pontos de alteração do comando SQL, feita pelo método HandleAuditInjection. Este último método primeiro identifica se a tabela que está sendo manipulada consta na lista de tabelas que devem ter log de auditoria (informação recebida no construtor da classe). Se constar, então identificamos qual comando que está sendo executado: se for um INSERT, UPDATE ou DELETE, encaminhamos o comando para o respectivo tratamento, onde identificamos em qual ponto do comando devemos incluir o comando OUTPUT. Se for INSERT ou UPDATE, então pegamos os registros da tabela virtual inserted. Se for DELETE, então pegamos da deleted. O ponto que inserimos o OUTPUT varia também: quando é UPDATE ou DELETE, colocamos antes do WHERE; no caso do INSERT, fica antes do VALUES. O horário da alteração é obtido simplesmente chamando DateTime.Now, e o usuário que faz a alteração através de Environment.UserName (para nosso exemplo, isso basta, mas em uma aplicação web, geralmente pegamos essa informação do token de autenticação).

Para configurar o Entity Framework para utilizar essa classe de interceptação, basta incluir uma instância da classe criada no objeto DbInterception.

using System.Data.Entity.Infrastructure.Interception;

namespace EF6AuditInterception
{
    class Program
    {
        static void Main(string[] args)
        {
            DbInterception.Add(new AuditInterceptor("People"));
            using (var context = new PersonDbContext())
            {
                var person = new Person() { FirstName = "Celes", LastName = "Chere" };
                context.People.Add(person);
                context.SaveChanges();
                person.Age = 17;
                context.SaveChanges();
                context.People.Remove(person);
                context.SaveChanges();
                var person2 = new Person() { FirstName = "Locke", LastName = "Cole" };
                context.People.Add(person2);
                context.SaveChanges();
            }
        }
    }
}

Uma coisa para se notar é que quando se configura uma classe para intercepção, ela atinge todas as operações que o Entity Framework faz a partir, pois ela é global. Pessoalmente eu acho isso ruim, deveria ser possível configurar por DbContext, mas isso pode ser uma funcionalidade futura. Ou como o Entity Framework tem código aberto, qualquer um pode sugerir essa alteração.

Com essa abordagem, conseguimos fazer a auditoria das alterações que ocorrem no sistema, mantendo a rastreabilidade do que ocorre.

[]'s

domingo, 4 de agosto de 2013

Instalação Windows 8 - No signed device drivers were found

Este final de semana chegou meu notebook novo que comprei (Dell Inspirion 15R). Veio com Windows 8 Single Language, mas como eu tenho uma licença Enterprise, resolvi reinstalar o sistema operacional por completo.

Coloquei o DVD do Windows 8 no drive, configurei o boot para iniciar pelo BD-ROM, e fui dando next durante o processo de instalação. Eis que no momento de escolher uma partição do HD para instalar o sistema operacional, não é listado nenhum e me aparece a mensagem abaixo.

"No signed device drivers were found. Make sure that the installation media contains the correct drivers, and then click OK".


Sem entender o problema, mexe aqui, mexe ali, tenta uma coisa e tenta outra, até que eu somo 1 + 1 e leio com atenção a mensagem. O problema é que o Windows 8 não reconheceu meu HD (mSATA), e por isso não tinha como encontrar a partição. Ora, para que ele pudesse ser visível, os drivers do HD deveriam estar junto no CD de instalação.

Bom, para resolver, coloquei em um pen drive conteúdo do DVD do Windows 8 de forma que fosse "bootável" (utilizei para isso o aplicativo "Windows 7 USB/DVD Download Tool" - detalhes de como fazer em http://pcsupport.about.com/od/windows-8/a/install-windows-8-usb.htm). Ainda neste pen drive, inclui os drivers necessários em uma pasta qualquer. Pra ser sincero, como não sabia exatamente qual drive era, coloquei todos relacionados ao chipset, de acordo com o CD de recursos que veio com o notebook.


Feito isso, dei boot pela USB, e dessa maneira as partições apareceram disponíveis para meu uso. A partir disso a instalação ficou simples.

[]'s

domingo, 9 de junho de 2013

Pex no Visual Studio 2012: Code Digger e TDD com teorias

Uma ferramenta que eu tenho acompanhado a evolução é o Pex (http://research.microsoft.com/en-us/projects/pex/). Trata-se de um projeto conduzido pela Microsoft Research desde os tempos de Visual Studio 2010. Como um subproduto deste projeto nasceu o Moles, um framework de isolamento de dependências, que na versão 2012 do Visual Studio tornou-se o Fakes.

Cheguei a publicar artigos e posts referente a essas ferramentas (Introdução ao framework de testes Microsoft Moles - Artigo Revista . Net Magazine 87, Geração automática de testes numa abordagem TDD - Revista .Net Magazine 90 e "Novidades" no Visual Studio 11 Beta - Microsoft Fakes). Confesso que a falta de novidades no Pex me deixou um pouco desanimado desde então, mas recentemente foi lançada uma primeira extensão dela voltada para o Visual Studio 2012: Code Digger!

Mas o que faz o Pex? Bom, ele é uma ferramenta de análise de código que busca encontrar todos os caminhos lógicos de forma a identificar valores que, quando passados para seus métodos, acabem gerando erros. Para fazer isso, ele analisa todas as possibilidades de instruções condicionais (if/else, while, etc.) dentro do seu código, sugerindo valores de parâmetros que sejam "interessantes". Por esta característica, podemos classificar o Pex como uma ferramenta de testes de caixa branca, ou seja, onde o teste é executado sobre uma implementação conhecida (diferente de testes de caixa preta, onde parte-se da premissa que não se conhece o que tem dentro de um objeto que será testado).

O Code Digger é uma extensão do Visual Studio 2012 que pode ser baixada em http://visualstudiogallery.msdn.microsoft.com/fb5badda-4ea3-4314-a723-a1975cbdabb4. Ele não tem todas as funcionalidades que tínhamos na versão 2010 do Pex, mas já é um começo. Após instalado, ele permite gerar uma tabela contendo os valores de parâmetros e respetivos resultados de determinado método para casos interessantes. Para isso, basta clicar com o botão direito sobre a função que queremos analisar, e selecionar a opção "Generate Inputs / Outputs Table".


Aqui tenho que fazer uma ressalva: na versão atual, o Code Digger dá suporte apenas para projetos do tipo Portable Class Library. Mas entendo que essa limitação logo será revista.

Para explicar, nada melhor que um exemplo. Imagine que estamos construindo um novo algoritmo de criptografia simétrico (que usa uma mesma chave para tanto cifrar quanto decifrar valores). A ideia desse algoritmo é transformar cada letra em um número, e separá-los pelo caractere de pipe ("|"). Ou seja, se tivermos o valor "AB" para criptografar, ele deve ser transformado em "14560|14850". Para não ficar tão fácil de descobrir o texto criptografado, nosso algoritmo permite que seja passada uma chave ("salt") para influenciar no valor final. Por exemplo, se o "salt" for 123, o resultado da criptografia de um caractere qualquer seria "12345"; se o "salt" for 456 o resultado final seria "7894", e assim por diante. Obviamente, se utilizarmos o "salt" errado na hora de descriptografar, não teremos o resultado correto.

A imagem abaixo descreve um pouco melhor esse algoritmo. Supondo o valor "AB" e um "salt" com o valor 224. Multiplicando o valor do caractere A na sua representação decimal (65), temos o valor 14560; somamos então +1 para o "salt", dando 225; multiplicamos o valor de B (66) por 225, dando 14850; somamos então +1 para o "salt"; multiplicamos o próximo caractere  por 226, e assim até terminar.


Para descriptografar, fazemos o processo ao contrário: 14560 dividido por 224 para dar 65; 14850 dividido por 225 para dar 66, etc.

Observação: apesar do cuidado de se utilizar uma chave "salt" para se ter resultados diferentes, a ideia desse algoritmo proposto poderia ser quebrada com um pouco de esforço. É claro que algoritmos simétricos da vida real possuem lógicas mais complexas, mas para o nosso exemplo esta lógica já basta.

Trabalhando numa abordagem TDD, podemos criar os seguintes métodos para começarmos a desenvolver nossa classe, baseado nos cenários que descrevemos:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using PexCodeDiggerExample;

namespace PexCodeDiggerExampleTest
{
    [TestClass]
    public class MySymmetricKeyAlgorithmTest
    {
        [TestMethod]
        public void ShouldCryptText()
        {
            var target = new MySymmetricKeyAlgorithm();
            var result = target.Crypt("AB", 224);
            Assert.AreEqual("14560|14850", result);
        }
        [TestMethod]
        public void ShouldDecryptText()
        {
            var target = new MySymmetricKeyAlgorithm();
            var result = target.Decrypt("14560|14850", 224);
            Assert.AreEqual("AB", result);
        }
    }
}

O primeiro método, ShouldCryptText, verifica se a lógica de criptografar que descrevemos está correto. O segundo, ShouldDecryptText, valida se o texto criptografado consegue retornar ao seu valor original após efetuarmos a descriptografia.

O código que desenvolvemos até agora está abaixo, lembrando que TDD é uma técnica iterativa, onde para cada novo teste temos uma versão diferente do código (até termos a versão final).

using System;
using System.Linq;

namespace PexCodeDiggerExample
{
    public class MySymmetricKeyAlgorithm
    {
        public string Crypt(string plainText, int salt)
        {
            char[] chars = plainText.ToCharArray();
            long[] values = new long[chars.Length];
            for (int i = 0; i < chars.Length; i++)
            {
                values[i] = chars[i] * salt;
                salt+= 1;
            }
            return string.Join("|", values);
        }
        public string Decrypt(string cipherText, int salt)
        {
            string[] values = cipherText.Split("|".ToCharArray(), StringSplitOptions.None);
            char[] chars = new char[values.Length];
            for (int i = 0; i < chars.Length; i++)
            {
                chars[i] = (char)(int.Parse(values[i]) / salt);
                salt+= 1;
            }
            return new string(chars);
        }
    }
}

Até aqui não vimos nada de novo. Vamos utilizar a ferramenta Code Digger em cima de um desses métodos, para vermos quais são os erros que ela retorna para nós. Podemos ver o resultado da análise do método Crypt na imagem abaixo.


Apenas um erro encontrado, onde o problema é simplesmente uma exceção de referência de objeto nulo. Aqui cabem algumas críticas que muitos fazem a respeito desse tipo de ferramenta, quando aplicado em desenvolvimento orientado a testes:

"Ferramentas como o Pex / Code Digger não são de muita serventia, pois basicamente permitem que seja obtida 100% de cobertura de testes dos códigos. As situações de erro não são muito significativas da parte do negócio, limitando-se basicamente a testar valores limites dos parâmetros."

Eu concordo com esta crítica. Mas o problema dela é que considera um uso incorreto deste tipo de ferramenta. Realmente não nos é dada nenhuma vantagem nos testes do ponto de vista de negócio, pois os erros que são pegos se resumem a codificação incorreta, como a do exemplo anterior, que poderia ser tratada com uma verificação de objeto nulo. Mas fica somente nisso.

Uma abordagem melhor para tratarmos o uso desse tipo de ferramenta é criar testes de unidade que ajam como teorias. Partimos então de uma afirmação na qual o Pex / Code Digger deve encontrar um caso negativo para invalidá-la. No momento que ele encontra esta situação, sabemos que nosso código possui um erro. Vejamos o exemplo abaixo (que foi criado como um método qualquer dentro do projeto do próprio algoritmo de criptografia, devido à limitação do Portable Class Library que já comentei; entretanto, esse tipo de código ficaria melhor como um teste dentro de um projeto de testes).

using System.Diagnostics.Contracts;

namespace PexCodeDiggerExample
{
    public class MySymmetricKeyAlgorithmTestTheory
    {
        public void EncipherAndDecipherATextWillResultInTheSameText(string plainText, int salt)
        {
            Contract.Assume(!string.IsNullOrEmpty(plainText));
            var target = new MySymmetricKeyAlgorithm();
            Contract.Assume(salt > 0);
            var cipherText = target.Crypt(plainText, salt);
            var plainTextAgain = target.Decrypt(cipherText, salt);
            Contract.Assert(plainTextAgain == plainText);
        }
    }
}

Nossa teoria aqui é que todo texto que é criptografado com uma determinada chave "salt", ao ser submetido ao processo de decriptação, deve sempre resultar no texto original. Ou seja, se criptografando "XYZ" resultar em "123|456|789", fazer a descriptografia de "123|456|789" deve resultar em "XYZ". Essa é a nossa teoria.

Note que estamos utilizando o Code Contracts (http://research.microsoft.com/en-us/projects/contracts/) para definir quais são as premissas da nossa teoria (texto original não nulo e a chave "salt" maior que zero) e também a forma como confirmamos a nossa teoria (valor descriptografado sendo igual ao original). Aqui vale citar que o Pex / Code Digger é um projeto bem "intímo" do Code Contracts, o que nos permite utilizá-lo para controlar a forma com que a análise de código leva o nosso código em consideração.

O resultado da execução do Code Digger, na imagem abaixo, mostra alguns erros que foram identificados no nosso código, considerando a nossa teoria. Podemos então concluir que nosso código ainda não está finalizado. Então, devemos corrigir o código, até erros não sejam mais identificados.


Por fim, gostaria de frisar que o uso do Pex / Code Digger não substituí os testes de unidade "manuais" que costumamos fazer. O que essa abordagem faz é complementar o TDD, nos dando mais uma opção para podermos identificar problemas no nosso código, que é basicamente a função de um teste.

[]'s

ATUALIZAÇÃO: Agora não existe mais a limitação de efetuar a exploração de código apenas em projetos do tipo Portable Class Library. Agora podemos colocar as teorias nos projetos de testes, que é o lugar mais apropriado.
Para isto, basta colocar a opção DisableCodeDiggerPortableClassLibraryRestriction como true, nas opções do Pex no Visual Studio.


sexta-feira, 3 de maio de 2013

Trocando configurações padrão do Live TIM

Hoje trocamos aqui em casa a Internet para o Live TIM.

A primeira coisa que fiz foi trocar as senhas, pois por padrão a rede wireless se chama LIVE TIM, com senha 12345678. Não estou muito a fim de compartilhar minha conexão com outras pessoas.

Para fazer isso, conectei na página de administração do modem (ZXDSL 931WII), cujo endereço (pelo menos para mim) foi http://192.168.1.1/. O usuário e senha padrões são admin / admin. Este também é bom trocar (no menu, Administration > User Management).

O nome da rede troquei no menu Network > WLAN > SSID Settings (campo SSID Name). Já a senha da rede foi trocada em Network > WLAN > Security (campo WPA Password).

Aproveitei e fiz um teste de velocidade em http://www.speedtest.net/. Ping de 11 ms, download de 37,65 MB, upload de 19,85 MB. Mas vamos ver como a TIM vai se comportar em relação a estabilidade do serviço (espero que não seja igual a qualidade da telefonia móvel).


[]'s

sábado, 15 de dezembro de 2012

Versão 1.0.1 do NHilo!

Hoje o post é para falar que está disponível a primeira versão estável da biblioteca NHilo (http://nhilo.codeplex.com/), componente para se gerar chaves primárias utilizando o algoritmo hilo do NHibernate!

As mudanças que ocorreram entre a versão beta e esta são:

  • Inclusão de providers para trabalhar com MySQL e Oracle.
  • Criação de um método de extensão para retornar um valor inteiro de 32-bits ao invés de um inteiro de 64-bits.
  • Mudanças de algumas classes para o namespace raiz.
  • Correção de alguns bugs referentes ao schema de configuração.
Por enquanto é só. A biblioteca pode ser baixada tanto nosite do Codeplex quanto pelo Nuget (www.nuget.org).

[]'s