Accès aux données dans les applications multi-utilisateurs

La question de la restriction de l'accès aux données se pose presque toujours lors du développement de systèmes multi-utilisateurs. Les principaux scénarios sont les suivants:

  1. restriction d'accès aux données pour les utilisateurs authentifiés
  2. restriction d'accès aux données pour les utilisateurs authentifiés mais ne disposant pas des privilèges nécessaires
  3. empêcher tout accès non autorisé via des appels directs à l'API
  4. filtrage des données dans les requêtes de recherche et liste des éléments d'interface utilisateur (tableaux, listes)
  5. empêcher la modification des données appartenant à un utilisateur par d'autres utilisateurs

Les scénarios 1 à 3 sont bien décrits et généralement résolus à l'aide des outils de structure intégrés, tels que l'autorisation basée sur les rôles ou basée sur les revendications . Mais des situations où un utilisateur autorisé peut accéder aux données d'un "voisin" par URL directe ou effectuer une action dans son compte se produisent tout le temps. Cela se produit le plus souvent du fait que le programmeur oublie d'ajouter la vérification nécessaire. Vous pouvez compter sur une révision de code ou vous pouvez éviter de telles situations en appliquant des règles de filtrage de données globales. Ils seront discutés dans l'article.

Listes et tableaux


Un contrôleur typique pour recevoir des données dans ASP.NET MVC pourrait ressembler à ceci :

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

Dans ce cas, la responsabilité du filtrage des données incombe uniquement au programmeur. Se souviendra-t-il qu'il est nécessaire d'ajouter une condition à Where ou non?

Vous pouvez résoudre le problème à l'aide de filtres globaux . Cependant, pour restreindre l'accès, nous avons besoin d'informations sur l'utilisateur actuel, ce qui signifie que la construction de DbContext devra être compliquée afin d'initialiser des champs spécifiques.

S'il existe de nombreuses règles, la mise en œuvre de DbContext devra inévitablement en apprendre «trop», ce qui violera le principe de la responsabilité exclusive .

Architecture de bouffée


Des problèmes d'accès aux données et de copier-coller se sont produits parce que dans l'exemple, nous avons ignoré la division en couches et des contrôleurs, nous avons immédiatement atteint la couche d'accès aux données, contournant la couche de logique métier. Cette approche a même été surnommée « des contrôleurs épais et stupides et laids ». Dans cet article, je ne veux pas aborder les problèmes liés aux référentiels, aux services et à la logique métier structurante. Les filtres globaux font un bon travail, il vous suffit de les appliquer à une abstraction d'une autre couche.

Ajouter une abstraction


.NET a déjà IQueryable pour accéder aux données. Remplacez l'accès direct à DbContext par l'accès à un tel fournisseur:

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

Et pour accéder aux données, nous allons faire ce filtre:

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

Nous implémentons le fournisseur de manière à ce qu'il recherche tous les filtres déclarés et les applique automatiquement:

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

Le code pour obtenir et créer des filtres dans l'exemple n'est pas optimal. Au lieu d' Activator.CreateInstance il est préférable d'utiliser des arbres d'expression compilés . Certains conteneurs du CIO prennent en charge l' enregistrement des génériques ouverts . Je laisserai les questions d'optimisation au-delà de la portée de cet article.

Nous réalisons des filtres


Une implémentation de filtre pourrait ressembler à ceci:

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

Nous corrigeons le code du contrôleur


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

Il n'y a pas beaucoup de changements. Il reste à interdire l'accès direct à DbContext depuis les contrôleurs et si les filtres sont correctement écrits, alors le problème d'accès aux données peut être considéré comme clos. Les filtres sont assez petits, il n'est donc pas difficile de les couvrir de tests. De plus, ces mêmes filtres peuvent être utilisés pour écrire un code d'autorisation qui empêche tout accès non autorisé à des données "étrangères". Je vais laisser cette question pour le prochain article.

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


All Articles