الوصول إلى البيانات في تطبيقات متعددة المستخدمين

تنشأ مسألة تقييد الوصول إلى البيانات دائمًا تقريبًا عند تطوير أنظمة متعددة المستخدمين. السيناريوهات الرئيسية هي كما يلي:

  1. تقييد الوصول إلى البيانات للمستخدمين المصادق عليهم
  2. تقييد الوصول إلى البيانات للمصادقة ولكن لا تمتلك الامتيازات اللازمة للمستخدمين
  3. منع الوصول غير المصرح به من خلال المكالمات المباشرة إلى API
  4. تصفية البيانات في استعلامات البحث وقائمة عناصر واجهة المستخدم (الجداول والقوائم)
  5. منع تغيير البيانات الخاصة بمستخدم واحد بواسطة مستخدمين آخرين

يتم وصف السيناريوهات 1-3 بشكل جيد ويتم حلها عادةً باستخدام أدوات الإطار المضمنة ، مثل التفويض القائم على الأدوار أو التفويض المستند إلى المطالبة . لكن المواقف التي يمكن فيها للمستخدم المصرح له الوصول مباشرة إلى بيانات "الجار" أو اتخاذ إجراء في حسابه تحدث طوال الوقت. يحدث هذا في أغلب الأحيان بسبب حقيقة أن المبرمج ينسى إضافة الفحص الضروري. يمكنك الاعتماد على مراجعة التعليمات البرمجية ، أو يمكنك منع مثل هذه المواقف من خلال تطبيق قواعد عالمية لتصفية البيانات. سيتم مناقشتها في المقالة.

القوائم والجداول


قد تبدو وحدة التحكم النموذجية لاستقبال البيانات في ASP.NET MVC على النحو التالي:

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

في هذه الحالة ، تقع جميع مسؤولية تصفية البيانات على المبرمج فقط. هل يتذكر أنه من الضروري إضافة شرط إلى Where أم لا؟

يمكنك حل المشكلة باستخدام المرشحات العالمية . ومع ذلك ، لتقييد الوصول ، نحتاج إلى معلومات حول المستخدم الحالي ، مما يعني أن بناء DbContext يجب أن يكون معقدًا من أجل تهيئة حقول معينة.

إذا كان هناك العديد من القواعد ، فإن تطبيق DbContext يجب أن يتعلم حتمًا "عدد كبير جدًا" ، مما ينتهك مبدأ المسؤولية الوحيدة .

العمارة النفخة


حدثت مشاكل في الوصول إلى البيانات ولصق النسخ لأننا في المثال تجاهلنا الفصل في طبقات ومن وحدات التحكم وصلنا على الفور إلى طبقة الوصول إلى البيانات ، متجاوزين طبقة منطق الأعمال. وقد أطلق على هذا النهج " وحدات تحكم قبيحة غبية ". في هذه المقالة ، لا أريد أن أتطرق إلى القضايا المتعلقة بالمستودعات والخدمات وهيكلة منطق الأعمال. تقوم المرشحات العالمية بعمل جيد في هذا الأمر ، ما عليك سوى تطبيقها على تجريد من طبقة أخرى.

أضف التجريد


.NET لديه IQueryable بالفعل للوصول إلى البيانات. استبدل الوصول المباشر إلى DbContext بالوصول إلى هذا المزود:

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

وللوصول إلى البيانات ، سنقوم بعمل هذا الفلتر:

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

نقوم بتنفيذ الموفر بطريقة يبحث عن جميع المرشحات المعلنة ونطبقها تلقائيًا:

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

رمز الحصول على الفلاتر وإنشائها في المثال ليس الأمثل. بدلاً من Activator.CreateInstance من الأفضل استخدام أشجار التعبير المجمعة . تدعم بعض حاويات IOC تسجيل الأدوية المفتوحة . سأترك أسئلة التحسين خارج نطاق هذه المقالة.

ندرك المرشحات


قد يبدو تنفيذ الفلتر كما يلي:

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

نقوم بتصحيح كود التحكم


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

لا توجد تغييرات كثيرة على الإطلاق. يبقى حظر الوصول المباشر إلى DbContext من وحدات التحكم وإذا كانت الفلاتر مكتوبة بشكل صحيح ، فيمكن اعتبار مشكلة الوصول إلى البيانات مغلقة. المرشحات صغيرة للغاية ، لذا فإن تغطيتها بالاختبارات ليست صعبة. بالإضافة إلى ذلك ، يمكن استخدام هذه الفلاتر نفسها لكتابة كود تفويض يمنع الوصول غير المصرح به إلى البيانات "الأجنبية". سأترك هذا السؤال للمقال التالي.

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


All Articles