DBAuthorization – Parte 3 – Estrutura do Banco de Dados

Como falado no post anterior, vamos utilizar uma base de dados como repositório para as políticas de acesso aos arquivos ASP.NET. Vale lembrar que você pode utilizar qualquer outro tipo de repositório, como por exemplo, um arquivo Xml independente.

É importante dizer que a partir da versão 2.0 do ASP.NET, a Microsoft criou diversas APIs para gerenciamento de usuários (Membership), de papéis (Roles) e de perfis de usuários (Profile). Essas funcionalidades foram extensamente discutidas neste artigo e já assumirei que você já tenha o respectivo conhecimento.

As tabelas auxiliares necessárias serão colocadas no mesmo banco de dados que já possua a infraestrutura das funcionalidades do ASP.NET que vimos no artigo mencionado acima. Basicamente teremos duas tabelas adicionais, sendo: aspnet_Authorization_Paths e aspnet_Authorization_Rules. Como podemos ter diversas políticas para uma página específica, então a primeira tabela será responsável por armazenar paths (páginas ou diretórios) que você deseja aplicar as políticas. Além do path (que deve incluir o diretório virtual), essa tabela terá uma coluna chamada ApplicationId, que é a chave estrangeira da tabela aspnet_Applications (built-in), já que podemos ter um banco de dados do ASP.NET compartilhado para várias aplicações. A segunda tabela, aspnet_Authorization_Rules, armazenará quais são as políticas definidas para um determinado path e, com isso, além de ter uma coluna para relacionamento com a tabela de paths, temos também: Action, Type e Data. A coluna Action determina qual a ação a ser executada (Deny ou Allow); já a coluna Type determina em quem vamos aplicar a ação (Users, Roles ou Verbs) e, finalmente, a coluna Data armazenará o nome dos usuários, papéis ou verbos que farão parte da política. A imagem abaixo ilustra o relacionamento entre elas:

Verbs: Esses são os conhecidos verbos de HTTP, e que neste caso faremos o uso GET ou POST. A idéia é também permitir o acesso somente quando a requisição for realizada através de um deles.

Além das tabelas ainda temos as Stored Procedures. Essas Stored Procedures apenas definem as queries necessárias para efetuar a inserção, exclusão ou a leitura das informações referentes as políticas de acesso. Para seguir a mesma convenção estipulada pela Microsoft (não que você deva seguí-la), as Stored Procedures estão prefixadas com a palavra “aspnet_”. Para atender as tarefas que vamos desempenhar, criei quatro delas, a saber:

 

  • aspnet_Authorization_AddRule: Adiciona uma nova regra.
  • aspnet_Authorization_DeletePathById: Exclui o path e todas as regras relacionadas.
  • aspnet_Authorization_DeleteRuleById: Exclui apenas uma única regra.
  • aspnet_Authorization_ReadRules: Extrai todas as regras definidas para uma aplicação.

Para poupar espaço, não vou colocar o código referente a cada Stored Procedure aqui. No final da série publicarei o projeto todo, incluindo o backup do banco de dados que possui a estrutura necessária para fazer tudo isso funcionar.

DBAuthorization – Parte 2 – A possível solução

Para solucionar o problema descrito no post anterior, podemos utilizar o banco de dados como repositório das políticas de acesso às páginas ASP.NET. Com isso, qualquer mudança que seja necessária, você pode utilizar os comandos (queries) e classes tradicionais de acesso à dados para fazer tal manipulação.

Mover para o banco de dados nos permitirá criar uma interface amigável para que o responsável pelo sistema gerencie as políticas de acesso. Como um ponto negativo desta técnica, temos o excesso de round-trips que haverá durante a execução da aplicação. Como todas as páginas ASP.NET podem ou não estar protegidas, será necessário consultar a base de dados para certificar que há alguma política definida para esta página e, se houver, aplicá-la.

Como as políticas de acesso são comuns para qualquer usuário, ou melhor, são configurações globais da aplicação e serão aplicadas/avaliadas em todas as requisições, podemos utilizar alguma estratégia de caching para evitar o constante acesso à base de dados. Mas esta técnica exige um certo sincronismo entre a leitura e escrita e que será discutido no momento correto.

DBAuthorization – Parte 1 – O problema

O ASP.NET introduziu novas formas de configurar e gerenciar a autenticação e autorização em aplicações Web. Temos o Windows Authentication e Forms Authentication para a autenticação e Url Authorization ou File Authorization para autorização.

Com Windows Authentication, o usuário apresentará o token do Windows que será validado em algum local (sendo na própria máquina ou em algum domínio). Já com o Forms Authentication, a aplicação irá gerenciar a identidade do usuário gravando-a em cookie durante o login e reutilizando-o nas chamadas subsequentes. Em um ambiente de internet, esse modelo é o mais utilizado.

Para autorização, temos duas possibilidades, sendo a primeira delas a File Authorization. Neste modo, o usuário somente terá acesso a um arquivo ASPX se o mesmo tiver permissão (ACL) de acesso. Este tipo de autorização é comumente utilizada em conjunto com a autenticação Windows (geralmente utilizado em intranets), onde você customizará as permissões fisicamente nos arquivos. No modo Url Authorization, a configuração de acesso é feito baseando-se na URL, e em um ambiente de internet, é também o que mais se utiliza.

O foco dos posts será a configuração da Url Authorization. Para customizar a restrição de cada arquivo, devemos recorrer ao arquivo Web.config, configurando a página ou diretório e quem pode ou não pode acessá-los. Aqui você poderá refinar a autorização baseando-se em usuários (users) ou em papéis (roles). A forma mais simples de conceder e negar acesso é fazendo uso dos papéis, pois nome de usuários mudam com muita frequência, e em pouco tempo deixam de existir. Como exemplo, o trecho de código abaixo ilustra uma possível configuração:

<?xml version=”1.0″?>
<configuration>
    <location path=”AreaRestrita”>
        <system.web>
            <authorization>
                <deny users=”?”/>
            </authorization>
        </system.web>
    </location>
</configuration>

Neste exemplo podemos notar que o diretório “AreaRestrita” é protegido contra usuários anônimos (representado pelo caractere “?”), ou seja, enquanto o usuário não estiver autenticado na aplicação, ele não terá acesso a qualquer conteúdo que está dentro deste diretório. Já no exemplo a seguir, apenas permitimos acesso à página “CriarUsuario.aspx” se o usuário fizer parte do papel (role) “Admin”:

<?xml version=”1.0″?>
<configuration>
    <location path=”CriarUsuario.aspx”>
        <system.web>
            <authorization>
                <allow roles=”Admin”/>
            </authorization>
        </system.web>
    </location>
</configuration>

Neste modelo de definição de autorização, temos um grande problema que é a atualização das informações, ou melhor, a alteração das políticas ali definidas. Podemos facilmente recorrer a API System.Xml que temos dentro do .NET Framework ou, de forma mais simples, utilizar as APIs de configuração que me permite o acesso tipado às seções do arquivo Web.config. Mas o problema maior não é isso, mas sim o transtorno que a alteração causa. Qualquer mudança no arquivo Web.config causará a reciclagem do processo do ASP.NET e, consequentemente, todas as informações que estão em memória, como o caching, variáveis estáticas, de sessão e aplicação são completamente perdidas, podendo ocasionar erros inesperados em tempo de execução e um comportamento indesejado para os usuários.

Particularmente eu acredito que essas políticas não mudam com muita frequência, mas recebi diversos e-mails e questões em fóruns perguntando sobre isso. De qualquer forma, resolvi criar uma forma customizada para definir as políticas de acesso às páginas ASP.NET. A partir da série de posts abaixo vou mostrar uma possível solução para este caso:

DBAuthorization – Parte 1 – O Problema
DBAuthorization – Parte 2 – A possível solução
DBAuthorization – Parte 3 – Estrutura do Banco de Dados
DBAuthorization – Parte 4 – Estrutura dos Tipos
DBAuthorization – Parte 5 – Provider
DBAuthorization – Parte 6 – Caching
DBAuthorization – Parte 7 – DBAuthorizationModule
DBAuthorization – Parte 8 – Configuração do arquivo Web.config
DBAuthorization – Parte 9 – Interface de Administração
DBAuthorization – Parte 10 – Conclusão

Paginação utilizando LINQ

Podemos facilmente vincular uma query LINQ em um controle DataBound do ASP.NET para exibir os dados. A query pode ser feita utilizando LINQ To SQL, LINQ To Xml, etc. Com isso, podemos simplesmente fazer:

this.GridView1.DataSource = from cliente in colecaoDeClientes select cliente;
this.GridView1.DataBind();

Os problemas aparecem quando voce habilita a paginação do controle e, ao rodar o mesmo exemplo, uma exceção do tipo NotSupportedException será disparada, informando a seguinte mensagem: The data source does not support server-side data paging. Quando efetuamos uma query LINQ (em cima de qualquer fonte de informação), o retorno é sempre uma classe que implementa direta ou indiretamente a Interface IEnumerable<T>, e para efetuar a paginação, é necessário que o controle conheça a quantidade de registros retornados pela query para conseguir dimensionar a quantidade necessária de páginas a serem exibidas.

A Interface referida acima não possui uma propriedade que informe a quantidade de elementos da coleção e, como alternativa, podemos alterar a query para que ela retorne uma instancia da classe List<T>, modificando ligeiramente a query:

this.GridView1.DataSource = (from cliente in colecaoDeClientes select cliente).ToList();
this.GridView1.DataBind();

Acessando tipos anônimos no evento RowDataBound

Há algum tempo eu mostrei como acessar o item que está sendo adicionado em um controle DataBound para fazer algum tipo de verificação durante a inserção dos dados no controle. Como é mostrado no post, a conversão deve ser feita de acordo com o tipo de fonte de dados que está sendo inserida no controle (DataTable, DataReader, objetos, etc). Mas e quando precisamos acessar um tipo anônimo que é projetado a partir de uma query LINQ?

this.GridView1.DataSource = from cliente in colecaoDeClientes
                            select new
                            {
                                Nome = cliente.Nome,
                                QualquerOutraInformacao = true
                            };
this.GridView1.DataBind();

No exemplo acima, não há um tipo efetivo em que podemos converter o DataItem que é passado como parametro para o evento, impossibilitando extrair o valor de uma determinada propriedade. Para contornar esse problema, podemos recorrer ao método estático Eval da classe DataBinder, passando o DataItem como parametro e o nome da propriedade a ser recuperada. O exemplo abaixo mostra como efetuar o acesso ao tipo anônimo no evento RowDataBound do controle GridView:

protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        ((Button)e.Row.FindControl(“btn”)).Enabled =
            Convert.ToBoolean(DataBinder.Eval(e.Row.DataItem, “QualquerOutraInformacao”));
    }
}

Além de ser uma técnica late-bound e que usa Reflection em runtime, ainda há o problema de ser fracamente tipado, o que nos obriga a sempre efetuar a conversão explícita para garantir a compilação da aplicação.

A necessidade do casting

Muitas vezes vejo em fóruns alguns trechos de códigos em que o pessoal faz o casting de um controle em outro para acessar uma determinada propriedade. O que quero chamar a atenção aqui é que nem sempre esses castings são necessários e, por menor que seja, efetuá-los sempre tem o seu custo.

Todo controle em ASP.NET herda direta ou indiretamente da classe Control. Essa classe fornece grande parte das propriedades e métodos que todo server-control deve ter. Imagine que dentro de um controle DataBound, como o ListView, voce possui um controle ASP.NET qualquer, e queira definir a sua propriedade Visible como False. Para exemplificar, notem que abaixo estou procurando pelo controle “Calendar1” através do método FindControl. Com o retorno deste método (que é um objeto derivado da classe Control) eu faço o casting para o controle Calendar e, consequentemente, defino a propriedade Visible como False.

protected void ListView1_ItemDataBound(object sender, ListViewItemEventArgs e)
{
    if (e.Item.ItemType == ListViewItemType.DataItem)
    {
        ((Calendar)e.Item.FindControl(“Calendar1”)).Visible = false;
    }
}

Como dito anteriormente, a propriedade Visible está definida na classe Control, e com isso não há necessidade de efetuar o casting para acessá-la. Voce só precisa efetuar o casting caso queira acessar uma propriedade exclusiva do controle. Para finalizar, abaixo temos o mesmo código sem o casting, e tendo o resultado conforme o esperado:

protected void ListView1_ItemDataBound(object sender, ListViewItemEventArgs e)
{
    if (e.Item.ItemType == ListViewItemType.DataItem)
    {
        e.Item.FindControl(“Calendar1”).Visible = false;
    }
}

Métodos assíncronos do ASP.NET Web Services

Ao fazer a referencia para um ASP.NET Web Services (ASMX), automaticamente o proxy é gerado. Antes do WCF, ao referenciar serviços ASMX a versão assíncrona (BeginXXX/EndXXX) dos métodos expostos por ele também eram criados.

Utilizando o Visual Studio .NET 2008, esse comportamento mudou um pouco. Agora temos uma opção chamada “Add Service Reference…” que, dado um endereço (seja ele para um serviço ASP.NET Web Services ou WCF), irá gerar o proxy. A questão é que este proxy baseia-se na infraestrutura do WCF (ClientBase<TChannel>), e a geração dos respectivos métodos assíncronos somente acontecerá se a opção “Generate asynchronous operations” do botão “Advanced” estiver selecionada (maiores detalhes neste artigo). Do contrário, a única forma assíncrona de trabalhar é utilizando o modelo de eventos.

Se quiser continuar gerando o proxy da forma antiga, ou seja, aquele que herda da classe SoapHttpClientProtocol, será necessário recorrer ao utilitário wsdl.exe, como é mostrado abaixo:

C:>wsdl http://localhost:54509/WebService1/Service.asmx /out:C:TempProxy.cs

requirePermission

As seções connectionStrings e appSettings são as únicas que podemos acessar diretamente no código sem nenhum tipo de problema, mesmo quando a aplicação está sendo executada em um ambiente parcialmente confiável. Por parcialmente confiável em uma uma aplicação ASP.NET, entenda como o trust level esteja definido como “Medium” ou algo abaixo disso:

<trust level=”Medium”/>

Neste caso, qualquer outra seção (authentication, smtp, globalization, etc.) que tentarmos acessar em um neste ambiente, uma exceção do tipo SecurityException será lançada informando que não possuímos a permissão necessária que, neste caso, é ConfigurationPermission. Note que só o fato de acessar a seção já provoca o disparo da exceção:

AuthenticationSection section =
    (AuthenticationSection)WebConfigurationManager.GetSection(“system.web/authentication”);
Response.Write(section.Mode.ToString());

Quem determina essa restrição é o atributo requirePermission que é configurado no registro da seção dentro do arquivo de configuração. Por padrão, ele é definido como True e, como dito acima, somente as seções connectionStrings e appSettings são definidas como False, como se pode notar no arquivo machine.config. Somente altere a configuração padrão caso voce realmente saiba o que está fazendo e, principalmente, conhecendo as implicações que isso pode causar. Além disso, é importante dizer que voce também pode determinar essa restrição quando voce cria uma seção de configuração customizada.

Globalização de arquivos *.sitemap

Em um dos treinamentos oficiais que ministro é abordado sobre globalização de aplicações. O capítulo foca nos tipos fornecidos pelo namespace System.Globalization, detalhando suas classes e como podemos utilizá-las para tornar o código independente de cultura.

Como o curso foca no .NET Framework e não na tecnologia, como ASP.NET ou Windows Forms, eu tento mostrar como isso funciona nas aplicações Web e Windows. Durante esse capítulo, uma pergunta que me foi feita é como efetuar a globalização de arquivos *.sitemap, que são utilizados pelos controles do ASP.NET para carregar Menus, Treeviews, etc.

Basicamente o que precisa ser feito é a criação do arquivo *.resx que representa a cultura, e lembrando que voce deve criar a quantidade necessária para todas as culturas suportadas pela aplicação. Neste caso, eu sugiro nomear os arquivos como: Sitemap.resx (default), Sitemap.en-US.resx, etc., e como eles serão utilizados por toda a aplicação, ficarão armazenados no diretório especial chamado App_GlobalResources. Dentro deste arquivo, como já é sabido, existe um dicionário (chave/valor). A chave identifica aquele valor unicamente dentro do arquivo, e será utilizada pela aplicação; já o valor é o variação para aquela cultura que o arquivo representa.

Depois de criado os arquivos *.resx, precisamos fazer com que o arquivo *.sitemap faça uso destas informações. Com isso, o atributo title do elemento siteMapNode não deve ter o título em hardcode, como acontece em aplicações não globalizadas. Ele utilizará um Expression Builder chamado $resources para dizer ao runtime do ASP.NET que esse valor será extraído de um arquivo de recurso. O que está em azul no exemplo abaixo reflete o nome do arquivo *.resx, enquanto o que está em vermelho é a chave.

<siteMapNode
    url=”~/AreaRestrita/Cadastros/Colaboradores/Default.aspx”
    title=”$resources:Sitemap, Colaboradores
    roles=”Cliente.Administrador” />

LINQ To SQL e Processamento Assíncrono do ASP.NET

escrevi e palestrei sobre a vantagem que temos ao fazer uso das páginas assíncronas, recurso que é fornecido a partir do ASP.NET 2.0.

Infelizmente o LINQ To SQL não possui intrinsicamente métodos para executar as queries de forma assíncrona e, sendo assim, não podemos incorporá-lo na execução assíncrona da página ASP.NET. Vale lembrar que voce pode criar um delegate apontando para um método, e dentro deste invocar as queries a partir do contexto do LINQ To SQL.

Utilizando esta técnica não trará os benefícios propostos pelas páginas assíncronas, pois quando voce invocar o delegate, ele extrairá uma thread do ThreadPool para executar a tarefa. A idéia das páginas assíncronas é fazer com que o processo custoso, como acesso a banco de dados, web services, etc., seja disparado através de uma thread de IO, liberando as threads do ThreadPool apenas para executar as páginas ASP.NET.

Para fazer o LINQ To SQL (ou qualquer outra tarefa) executar em uma thread de IO, podemos recorrer ao Power Threading, criada pela Wintellect. Essa library possui várias classes que nos auxiliam em tarefas assíncronas e, entre elas, temos a classe chamada CallbackThreadPool, que encapsula e gerencia a execução de tarefas a partir de threads de IO. Um único detalhe que precisamos nos atentar é a criação de um IAsyncResult customizado, que será utilizado pelo ASP.NET para determinar quando a query executada pelo LINQ To SQL for finalizada.

A parte mais complexa é a criação do IAsyncResult. Além da sua principal finalidade, ele também trará o resultado do processo assíncrono e possíveis exceções que essa tarefa possa disparar. Para facilitar e também conseguir reutilizar essa classe por várias entidades, eu a criei de forma genérica, trabalhando fortemente tipada. Para poupar espaço, abaixo consta apenas a sua definição.

public class DataAsyncResult<TResult> : IAsyncResult
{
    //implementação
}

A classe CallbackThreadPool fornece um método QueueUserWorkItem, e recebe como parametro uma instancia do delegate WaitCallback e da classe DataAsyncResult. O delegate deverá apontar para o método que irá executar a query via LINQ To SQL. O código abaixo ilustra como proceder para alistar um novo processo assíncrono através da Power Threading da Wintellect:

DataAsyncResult<IEnumerable<Cliente>> ar = new DataAsyncResult<IEnumerable<Cliente>>(callback, state);
_threadPool.QueueUserWorkItem(new WaitCallback(RecuperarDados), ar);

A partir deste ponto tudo é como já acontece normalmente com uma página assíncrona, ou seja, definindo o atributo Async da diretiva @Page como True e registrar a execução do processo assíncrono através do método AddOnPreRenderCompleteAsync da classe Page. Código de exemplo:

AsyncLINQToSQL.zip (39.97 kb)