
El problema de restringir el acceso a los datos surge casi siempre cuando se desarrollan sistemas multiusuario. Los escenarios principales son los siguientes:
- restricción de acceso a datos para usuarios autenticados
- restricción de acceso a los datos para usuarios autenticados pero que no poseen los privilegios necesarios de los usuarios
- evitar el acceso no autorizado a través de llamadas directas a la API
- Filtrar datos en consultas de búsqueda y elementos de IU de lista (tablas, listas)
- 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() .Count(); var items= _dbContext .Set<TEntity>() .Where() .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 {
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() .Count(); var items = QueryableProvider .Query<TEntity>() .Where() .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.