Acesso a dados em aplicativos multiusuários

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:

  1. restrição de acesso a dados para usuários autenticados
  2. restrição de acesso a dados autenticados, mas sem os privilégios necessários dos usuários
  3. impedir acesso não autorizado por meio de chamadas diretas à API
  4. filtrar dados em consultas de pesquisa e listar elementos da interface do usuário (tabelas, listas)
  5. 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(/* some business rules */) .Count(); var items= _dbContext .Set<TEntity>() .Where(/* some business rules */) .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 { //       private static Type[] Filters = typeof(PermissionFilter<>) .Assembly .GetTypes() .Where(x => x.GetInterfaces().Any(y => y.IsGenericType && y.GetGenericTypeDefinition() == typeof(IPermissionFilter<>))) .ToArray(); private readonly DbContext _dbContext; private readonly IIdentity _identity; public QueryableProvider(DbContext dbContext, IIdentity identity) { _dbContext = dbContext; _identity = identity; } private static MethodInfo QueryMethod = typeof(QueryableProvider) .GetMethods() .First(x => x.Name == "Query" && x.IsGenericMethod); private IQueryable<T> Filter<T>(IQueryable<T> queryable) => Filters //     .Where(x => x.GetGenericArguments().First() == typeof(T)) //         Queryable<T> .Aggregate(queryable, (c, n) => ((dynamic)Activator.CreateInstance(n, _dbContext, _identity)).GetPermitted(queryable)); public IQueryable<T> Query<T>() where T : class => Filter(_dbContext.Set<T>()); //  EF Core  Set(Type type),    :( public IQueryable Query(Type type) => (IQueryable)QueryMethod .MakeGenericMethod(type) .Invoke(_dbContext, new object[]{}); } 

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(/* some business rules */) .Count(); var items = QueryableProvider .Query<TEntity>() .Where(/* some business rules */) .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.

Source: https://habr.com/ru/post/pt414897/


All Articles