Acceso a datos en aplicaciones multiusuario

El problema de restringir el acceso a los datos surge casi siempre cuando se desarrollan sistemas multiusuario. Los escenarios principales son los siguientes:

  1. restricción de acceso a datos para usuarios autenticados
  2. restricción de acceso a los datos para usuarios autenticados pero que no poseen los privilegios necesarios de los usuarios
  3. evitar el acceso no autorizado a través de llamadas directas a la API
  4. Filtrar datos en consultas de búsqueda y elementos de IU de lista (tablas, listas)
  5. evitar el cambio de datos que pertenecen a un usuario por otros usuarios

Los escenarios 1-3 se describen bien y generalmente se resuelven utilizando las herramientas de marco integradas, como la autorización basada en roles o en notificaciones. Pero las situaciones en las que un usuario autorizado puede acceder directamente a los datos de un "vecino" o realizar una acción en su cuenta todo el tiempo. Esto ocurre con mayor frecuencia debido al hecho de que el programador olvida agregar la verificación necesaria. Puede confiar en una revisión de código, o puede prevenir tales situaciones aplicando reglas globales de filtrado de datos. Se discutirán en el artículo.

Listas y tablas


Un controlador típico para recibir datos en ASP.NET MVC podría verse así :

[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}); } 

En este caso, toda la responsabilidad de filtrar los datos recae solo en el programador. ¿Recordará que es necesario agregar una condición a Where o no?

Puede resolver el problema utilizando filtros globales . Sin embargo, para restringir el acceso, necesitamos información sobre el usuario actual, lo que significa que la construcción de DbContext tendrá que ser complicada para inicializar campos específicos.

Si hay muchas reglas, entonces la implementación de DbContext inevitablemente tendrá que aprender "demasiadas", lo que violará el principio de responsabilidad exclusiva .

Arquitectura de hojaldre


Se produjeron problemas con el acceso a los datos y al copiar y pegar porque en el ejemplo ignoramos la división en capas y desde los controladores llegamos inmediatamente a la capa de acceso a los datos, evitando la capa de lógica de negocios. Este enfoque incluso ha sido denominado " controladores gruesos, tontos y feos ". En este artículo no quiero tocar temas relacionados con repositorios, servicios y estructuración de lógica de negocios. Los filtros globales hacen un buen trabajo de esto, solo necesita aplicarlos a una abstracción de otra capa.

Añadir abstracción


.NET ya tiene IQueryable para acceder a los datos. Reemplace el acceso directo a DbContext con acceso a dicho proveedor:

  public interface IQueryableProvider { IQueryable<T> Query<T>() where T: class; IQueryable Query(Type type); } 

Y para acceder a los datos, haremos este filtro:

  public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); } 

Implementamos el proveedor de tal manera que busca todos los filtros declarados y los aplica automáticamente:

  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[]{}); } 

El código para obtener y crear filtros en el ejemplo no es óptimo. En lugar de Activator.CreateInstance es mejor usar árboles de expresión compilados . Algunos contenedores del COI admiten el registro de genéricos abiertos . Dejaré las preguntas de optimización más allá del alcance de este artículo.

Realizamos filtros


Una implementación de filtro podría verse así:

  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)); } } 

Corregimos el código del 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}); } 

No hay muchos cambios en absoluto. Queda por prohibir el acceso directo a DbContext desde los controladores y si los filtros están escritos correctamente, entonces el problema del acceso a los datos puede considerarse cerrado. Los filtros son bastante pequeños, por lo que cubrirlos con pruebas no es difícil. Además, estos mismos filtros se pueden usar para escribir un código de autorización que evite el acceso no autorizado a datos "extraños". Dejaré esta pregunta para el próximo artículo.

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


All Articles