
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:
- restriction d'accès aux données pour les utilisateurs authentifiés
- restriction d'accès aux données pour les utilisateurs authentifiés mais ne disposant pas des privilèges nécessaires
- empêcher tout accès non autorisé via des appels directs à l'API
- filtrage des données dans les requêtes de recherche et liste des éléments d'interface utilisateur (tableaux, listes)
- 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() .Count(); var items= _dbContext .Set<TEntity>() .Where() .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 {
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() .Count(); var items = QueryableProvider .Query<TEntity>() .Where() .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.