Datenzugriff in Mehrbenutzeranwendungen

Das Problem der Einschränkung des Zugriffs auf Daten tritt fast immer bei der Entwicklung von Mehrbenutzersystemen auf. Die Hauptszenarien sind wie folgt:

  1. Datenzugriffsbeschränkung für authentifizierte Benutzer
  2. Einschränkung des Zugriffs auf Daten für authentifizierte Benutzer, die jedoch nicht über die erforderlichen Berechtigungen der Benutzer verfügen
  3. Verhindern Sie unbefugten Zugriff durch direkte Aufrufe der API
  4. Filtern von Daten in Suchanfragen und Listen-UI-Elementen (Tabellen, Listen)
  5. Verhindern der Änderung von Daten, die einem Benutzer gehören, durch andere Benutzer

Die Szenarien 1 bis 3 sind gut beschrieben und werden normalerweise mithilfe der integrierten Framework-Tools wie rollenbasierter oder anspruchsbasierter Autorisierung gelöst. Es kommt jedoch immer wieder vor, dass ein autorisierter Benutzer über eine direkte URL auf die Daten eines "Nachbarn" zugreifen oder eine Aktion in seinem Konto ausführen kann. Dies geschieht meistens aufgrund der Tatsache, dass der Programmierer vergisst, die erforderliche Prüfung hinzuzufügen. Sie können sich auf eine Codeüberprüfung verlassen oder solche Situationen verhindern, indem Sie globale Datenfilterregeln anwenden. Sie werden im Artikel besprochen.

Listen und Tabellen


Ein typischer Controller zum Empfangen von Daten in ASP.NET MVC sieht möglicherweise folgendermaßen aus:

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

In diesem Fall liegt die Verantwortung für das Filtern der Daten nur beim Programmierer. Wird er sich daran erinnern, dass es notwendig ist, eine Bedingung zu Where hinzuzufügen oder nicht?

Sie können das Problem mithilfe globaler Filter lösen. Um den Zugriff einzuschränken, benötigen wir jedoch Informationen über den aktuellen Benutzer. DbContext bedeutet, dass die Erstellung von DbContext kompliziert sein muss, um bestimmte Felder zu initialisieren.

Wenn es viele Regeln gibt, muss die Implementierung von DbContext zwangsläufig „zu viele“ lernen, was gegen das Prinzip der alleinigen Verantwortung verstößt.

Puff Architektur


Probleme beim Zugriff auf Daten und beim Kopieren und Einfügen traten auf, weil wir im Beispiel die Unterteilung in Schichten ignorierten und von den Controllern sofort die Datenzugriffsschicht erreichten, wobei die Geschäftslogikschicht umgangen wurde. Dieser Ansatz wurde sogar als " dicke, dumme, hässliche Controller " bezeichnet. In diesem Artikel möchte ich nicht auf Probleme im Zusammenhang mit Repositorys, Diensten und der Strukturierung der Geschäftslogik eingehen. Globale Filter leisten hier gute Arbeit. Sie müssen sie nur auf eine Abstraktion von einer anderen Ebene anwenden.

Abstraktion hinzufügen


.NET verfügt bereits über IQueryable für den Zugriff auf Daten. Ersetzen Sie den direkten Zugriff auf DbContext durch den Zugriff auf einen solchen Anbieter:

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

Um auf die Daten zuzugreifen, erstellen wir diesen Filter:

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

Wir implementieren den Anbieter so, dass er nach allen deklarierten Filtern sucht und diese automatisch anwendet:

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

Der Code zum Abrufen und Erstellen von Filtern im Beispiel ist nicht optimal. Anstelle von Activator.CreateInstance es besser, kompilierte Ausdrucksbäume zu verwenden. Einige IOC-Container unterstützen die Registrierung offener Generika . Ich werde Optimierungsfragen über den Rahmen dieses Artikels hinaus belassen.

Wir realisieren Filter


Eine Filterimplementierung könnte folgendermaßen aussehen:

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

Wir korrigieren den Controller-Code


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

Es gibt überhaupt nicht viele Änderungen. Es bleibt weiterhin der direkte Zugriff von Controllern auf DbContext zu verbieten. Wenn die Filter korrekt geschrieben sind, kann das Problem des Datenzugriffs als geschlossen betrachtet werden. Die Filter sind recht klein, so dass es nicht schwierig ist, sie mit Tests abzudecken. Darüber hinaus können dieselben Filter verwendet werden, um einen Autorisierungscode zu schreiben, der den unbefugten Zugriff auf "fremde" Daten verhindert. Ich werde diese Frage für den nächsten Artikel belassen.

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


All Articles