
A questão da restrição do acesso aos dados surge quase sempre no desenvolvimento de sistemas multiusuários. Os principais cenários são os seguintes:
- restrição de acesso a dados para usuários autenticados
- restrição de acesso a dados autenticados, mas sem os privilégios necessários dos usuários
- impedir acesso não autorizado por meio de chamadas diretas à API
- filtrar dados em consultas de pesquisa e listar elementos da interface do usuário (tabelas, listas)
- impedindo a alteração de dados pertencentes a um usuário por outros usuários
Os cenários 1-3 são bem descritos e geralmente resolvidos usando as ferramentas da estrutura interna, como autorização
baseada em
função ou
declaração . Porém, situações em que um usuário autorizado pode acessar diretamente os dados de um "vizinho" ou executar uma ação em sua conta acontecem o tempo todo. Isso acontece com mais freqüência devido ao fato de o programador esquecer de adicionar a verificação necessária. Você pode confiar em uma revisão de código ou pode evitar tais situações aplicando regras globais de filtragem de dados. Eles serão discutidos no artigo.
Listas e tabelas
Um controlador típico para receber dados no ASP.NET MVC pode ser algo
como isto :
[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = _dbContext .Set<TEntity>() .Where() .Count(); var items= _dbContext .Set<TEntity>() .Where() .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }
Nesse caso, toda a responsabilidade de filtrar os dados fica apenas com o programador. Ele lembrará que é necessário adicionar uma condição a
Where
ou não?
Você pode resolver o problema usando
filtros globais . No entanto, para restringir o acesso, precisamos de informações sobre o usuário atual, o que significa que a construção do
DbContext
terá que ser complicada para inicializar campos específicos.
Se houver muitas regras, a implementação do
DbContext
inevitavelmente precisará aprender "muitas", o que violará o
princípio da responsabilidade exclusiva .
Arquitetura Puff
Ocorreram problemas com o acesso aos dados e copiar e colar porque no exemplo ignoramos a divisão em camadas e, a partir dos controladores, atingimos imediatamente a camada de acesso a dados, ignorando a camada da lógica de negócios. Essa abordagem foi até apelidada de "
controladores feios e grossos ". Neste artigo, não quero abordar questões relacionadas a repositórios, serviços e estruturação da lógica de negócios. Os filtros globais fazem um bom trabalho nisso, basta aplicá-los a uma abstração de outra camada.
Adicionar abstração
O .NET já possui o
IQueryable
para acessar dados. Substitua o acesso direto ao
DbContext
pelo acesso a esse provedor:
public interface IQueryableProvider { IQueryable<T> Query<T>() where T: class; IQueryable Query(Type type); }
E para acessar os dados, criaremos este filtro:
public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); }
Implementamos o provedor de forma que ele procure por todos os filtros declarados e os aplique automaticamente:
public class QueryableProvider: IQueryableProvider {
O código para obter e criar filtros no exemplo não é o ideal. Em vez de
Activator.CreateInstance
é melhor usar
árvores de expressão compiladas . Alguns contêineres do COI oferecem suporte ao
registro de genéricos abertos . Deixarei as questões de otimização além do escopo deste artigo.
Percebemos filtros
Uma implementação de filtro pode ser assim:
public class EntityPermissionFilter: PermissionFilter<Entity> { public EntityPermissionFilter(DbContext dbContext, IIdentity identity) : base(dbContext, identity) { } public override IQueryable<Practice> GetPermitted( IQueryable<Practice> queryable) { return DbContext .Set<Practice>() .WhereIf(User.OrganizationType == OrganizationType.Client, x => x.Manager.OrganizationId == User.OrganizationId) .WhereIf(User.OrganizationType == OrganizationType.StaffingAgency, x => x.Partners .Select(y => y.OrganizationId) .Contains(User.OrganizationId)); } }
Corrigimos o código do controlador
[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = QueryableProvider .Query<TEntity>() .Where() .Count(); var items = QueryableProvider .Query<TEntity>() .Where() .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }
Não há muitas mudanças. Resta proibir o acesso direto ao
DbContext
de controladores e, se os filtros forem gravados corretamente, a questão do acesso a dados poderá ser considerada encerrada. Os filtros são muito pequenos, portanto, cobri-los com testes não é difícil. Além disso, esses mesmos filtros podem ser usados para escrever um código de autorização que impede o acesso não autorizado a dados "estrangeiros". Vou deixar essa pergunta para o próximo artigo.