در بسیاری موارد (مانند سیستمهای Multi Tenant) لازم هست تا مانع از این شویم که دادههای کاربران با هم تداخل پیدا کند و یا آنها بتوانند به دادههای هم دسترسی داشته باشند. مثلا میخواهیم کاربران هر شعبه از سازمان، تنها به اطلاعات شعبه خودشان دسترسی داشته باشند. یک کار ساده، پردردسر و بسیار بد آن است که از برنامه نویسها بخواهیم در هر کوئری عبارتی را اضافه کنند که سطح دسترسی را چک کند. اما اگر برنامه نویس جایی فراموش کرد چی؟ اگر سیاست دسترسی پیچیدهتر بود و مبنی بر پارامترهای مختلف محاسبه میشد چه خواهد شد؟ این راهکار در حجم بزرگ غیر مطمئن و غیرقابل نگهداری است.
در EF6 قابلیتی به نام Interception وجود دارد که با استفاده از آن میتوان سیاست دسترسی به داده را در لایههای پایینی طراحی کرد. در این روش برنامه نویس لایه هایی بالا، بدون آنکه درگیر مفاهیمی مانند Tenant و سیاستها بشود، میتواند به راحتی کوئری هایش را تولید کند. سپس EF به طور خودکار تغییری در کوئریها خواهد داد تا دسترسیهای لازم رعایت کرده باشد. برای اینکار میتوانید از کتابخانه EntityFramework.DynamicFilters استفاده کنید.
این روش هم علی رغم همه مزایا معایبی هم دارد. اگر بخواهیم از همین پایگاه داده استفاده کنیم ولی در محیط دات نت نباشیم و یا از EF6 استفاده نکنیم، دوباره مشکلات اغاز میشوند. سیاستها را باید در همه جا کپی کنیم و در صورت لزوم هم، مجددا همه را تغییر دهیم.
در SQL Server 2016 قابلیتی به نام Row Level Security وجود دارد، که به ما اجازه میدهد سیاستهای دسترسی با داده را در لایه پایگاه داده متمرکز کنیم. در این صورت اپلیکشنها هیچگونه آگاهی ایی نسبت به سیاستها نخواهند داشت و درگیر این مفاهیم در سطح کد نخواهیم بود. همچنین در صورت لزوم به تغییر سیاست ها، فقط لازم است تغییراتی را در پایگاه داده بدهیم. با این روش، به هر طریقی و از هر ابزاری که به پایگاه داده کوئری هایمان را ارسال کنیم، سیاستهای دسترسی به داده اعمال خواهند شد و امنیت بالا و البته ریزدانه ای (granular) را خواهیم داشت.
در مثال زیر خواهیم دید که چگونه میتوان با استفاده از EF6 از ویژگی RLS بهره برد. این مثال یکی دیگر از کاربردهای Interception را نیز توضیح میدهد.
نظرات مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
در مثال دات نت کور این مساله در بخش Sample2 داخل کنترلر دو پارامتر رو(string param1, string param2) ذکر کردین که تو View همون کنترلر اشاره شده که میشه ارسال اطلاعات اضافی کرد به سمت سرور توسط این بخش ارسال اطلاعات اضافی و سفارشی به سرور در حین درخواست . ولی من میخوام یه ای دی از نوع int رو این شکلی ارسال کنم ولی هر بار که چک میکنم نال میفرسته داخل کنترلر.
این متد برای kendo grid بخش read هستش داخل کنترلر :
public IActionResult CurrentPostTags(int blogID) { var dataString = this.HttpContext.GetJsonDataFromQueryString(); var request = JsonConvert.DeserializeObject<DataSourceRequest>(dataString); var tags = _blogRepository.GetAllTags(blogID).OrderBy(s => s.Id); var currenttags = _mapper.Map<IList<TagsViewModel>>(tags); if (currenttags == null) { return NotFound(); } return Json(currenttags.AsQueryable() .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter)); }
که میخوام blogID رو دریافت کنم از سمت کندو گرید ولی نال میگیره هر بار. حتی یه رشته هم میفرستم باز نال برمیگردونه.
اینم گرید بنده :
$(function() { var currentTagsDataSource = new kendo.data.DataSource({ transport: { read: { url: "@Url.Action("CurrentPostTags", "Admin")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET', data : { blogID: @BlogID } },
- به عنوان مثال در بسیاری از متدهای لایه سرویس نیاز به چنین بررسی میباشد و ابتدای متدها این قضیه ابتدا چک میشود. این متد به عنوان یک متد Domain Service صرفا در لایه سرویس استفاده میشود.
در پاسخ به ادامه سوال یک باید عرض کنم بله وقتی کاربری یافت نشد چه کاری میتوانیم انجام دهیم؟ وظیفه متد مورد نظر بازگشت دادن یک کاربر بود نه مقدار Null و در اینجا صرفا یک Exception سفارشی صادر شده تا در بالاترین لایه مدیریت شود و پیغامی مناسب به کاربر نشان داده شود یا ...
[Transactional] public async Task CreateAsync(OrganizationalUnitCreateModel model) { Guard.ArgumentNotNull(model, nameof(model)); if (model.ParentId.HasValue) await _manager.CheckIsDeactiveAsync(model.ParentId.Value).ConfigureAwait(false); //... }
توجه شما را جلب میکنم به مطلب :نکات کار با استثناءها در دات نت
به این چنین کدهایی معمولاً The null cancer گفته میشود (سرطان نال!) زیرا اجازه دادهایم متد، خروجی null را بازگشت دهد.
- خیلی موافق نیستم با صحبت شما؛ من از استثناها برای ارسال پیغامی از داخلیترین لایه به خارجیترین لایه استفاده میکنم بجای بازگشت خروجی مثل OperationResult به لایههای بالاتر به صورت زنجیره ای. رکورد کاربری در دیتبایس موجود نیست را شما پیش بینی میکنید؟ من در متن مطلب هم اشاره کردم به عنوان ابزاری برای هدف خاصی استفاده میکنم از استثنا ها.
- بله درست است. ولی این الگوهایی که نام بردید در یک پروژه بزرگ تعداد خط کد را خیلی بالا خواهد برد.
این روش برای مدیریت این چنین کارها به مراتب خیلی ساده بوده و بحث fail-fast را دنبال میکند.
نظرات مطالب
پیاده سازی Option یا Maybe در #C
در اولین کد نوشته اید:
اگر user==null باشد خطای HttpNotFound و در غیراینصورت ویوی متناظر برگشت داده شود.
در ادامه "نوشتن if اضافه جهت چک کردن نال رفرنس" را بعنوان ایراد مطرح کردید.
اما در دومین کد، رفتار متد تغییر کرد تا if حذف شود (در هر صورت یک ویو برگشت داده میشود).
بجای
نوشتید:
اگر هدف متد؛ انجام کار به شکل کد دوم باشد میتوان همان متد اولیه را بدون استفاده از Maybe و بصورت تک خطی نوشت:
هم کدها ساده شده اند و هم if و خیلی چیزهای دیگه حذف شده اند.
بهرحال در نسخه نهایی بررسی نال رفرنس همچنان وجود دارد و تنها خودش رو به شکل دیگری بروز میدهد: DefaultIfEmpty(new User()).
آیا با این روش فقط کدها را پیچیدهتر نمیکنیم؟ (نقض KISS )
آیا میشود همان منطق اولیه را با این روش انجام داد؟ (بدون پاسکاری یا موکول کردن آن به متد دیگر)
آیا در کل لزومی به استفاده از Maybe یا Option هست؟
...
اگر user==null باشد خطای HttpNotFound و در غیراینصورت ویوی متناظر برگشت داده شود.
در ادامه "نوشتن if اضافه جهت چک کردن نال رفرنس" را بعنوان ایراد مطرح کردید.
اما در دومین کد، رفتار متد تغییر کرد تا if حذف شود (در هر صورت یک ویو برگشت داده میشود).
بجای
public ActionResult Details(int id) { var user=_userService.GetById(3); // این متد ممکن است مقداری برگرداند و یا مقدار نال برگرداند if( user == null) return HttpNotFound(); return View(user); }
public ActionResult Details(int id) { var user = _userService .GetById(3) .DefaultIfEmpty(new User()) .Single(); return View(user); }
اگر هدف متد؛ انجام کار به شکل کد دوم باشد میتوان همان متد اولیه را بدون استفاده از Maybe و بصورت تک خطی نوشت:
public ActionResult Details(int id) { return View(_userService.GetById(3) ?? new User()); }
بهرحال در نسخه نهایی بررسی نال رفرنس همچنان وجود دارد و تنها خودش رو به شکل دیگری بروز میدهد: DefaultIfEmpty(new User()).
آیا با این روش فقط کدها را پیچیدهتر نمیکنیم؟ (نقض KISS )
آیا میشود همان منطق اولیه را با این روش انجام داد؟ (بدون پاسکاری یا موکول کردن آن به متد دیگر)
public ActionResult Details(int id) { var user = _userService.GetById(3); return user == null ? HttpNotFound() : View(user); }
آیا در کل لزومی به استفاده از Maybe یا Option هست؟
...
آقای نصیری ، پیرو این بازخورد ، از شما تشکر میکنم که پاسخ دادید و مشخص شد مشکل از کدهای من بوده و درخواستها واقعا ارسال میشه. ولی مشکل من هنوز پابرجاست. چون ربطی به بازخورد قبلی نداره این "نظر ارسالی" من ، برای همین مشکلم رو در اینجا دوباره بیان میکنم.
من کدهای این مطلب رو در برنامه قرار دادم ولی مشکل هنوز پا برجاست. در قسمت سابقه متدها چک کردم و متوجه شدم از کلاس CustomSecurityStampValidator درخواستها ارسال میشن :
و برای فراخوانی :
لطفا اگر اطلاع دارید که چرا این اتفاق ممکنه رخ میده راهنمایی کنید. چرا در اونجا رد میشن ولی باز در این کلاس فراخوانی میشن.
من کدهای این مطلب رو در برنامه قرار دادم ولی مشکل هنوز پا برجاست. در قسمت سابقه متدها چک کردم و متوجه شدم از کلاس CustomSecurityStampValidator درخواستها ارسال میشن :
همونطور که مشاهده میکنید درخواستها درست است در Application_AuthenticateRequest رد میشن ولی در اینجا نیز فراخوانی میشن و یا به عبارتی یک کوئری برای هر فایل استایک روی بانک اجرا میشه.
واقعیت امر اینه اصلا نمیدونم چرا این اتفاق رخ میده ولی یک متد با کدهای شما در کلاس CustomSecurityStampValidator قرار دادم و فراخوانی کردم اون رو و مشکل حل شد و دیگه درخواستی ارسال نمیشه و همهی درخواستهای فایلهای مورد نظر رد میشن. به این نحو :
private static bool ShouldIgnoreRequest(CookieValidateIdentityContext context) { string[] reservedPath = { "/__browserLink", "/img", "/fonts", "/Scripts", "/Content", "/Uploads", "/Images" }; return reservedPath.Any( path => context.OwinContext.Request.Path.Value.StartsWith(path, StringComparison.OrdinalIgnoreCase)) || BundleTable.Bundles.Select(bundle => bundle.Path.TrimStart('~')) .Any( bundlePath => context.OwinContext.Request.Path.Value.StartsWith(bundlePath, StringComparison.OrdinalIgnoreCase)); }
//... if (ShouldIgnoreRequest(context)) return; var manager = context.OwinContext.GetUserManager<ApplicationUserManager>(); var userId = getUserIdCallback(context.Identity); //...
لطفا اگر اطلاع دارید که چرا این اتفاق ممکنه رخ میده راهنمایی کنید. چرا در اونجا رد میشن ولی باز در این کلاس فراخوانی میشن.
در ابتدا بهتر است با فایلهای packages.config و repositories.config آشنا شویم.
فایل packages.config در ازای هر پروژه ایجاد میشود و در این فایل اطلاعات package هایی که به پروژه اضافه شده اند نگهداری میشوند.
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="Microsoft.Owin" version="3.0.0" targetFramework="net45" /> <package id="RavenDB.Client" version="2.0.2375" targetFramework="net45" /> </packages>
<?xml version="1.0" encoding="utf-8"?> <repositories> <repository path="..\Application\Test\packages.config" /> <repository path="..\ViewModel\Test\packages.config" /> </repositories>
حال هر شخصی که پروژه را get میکند نیاز است با توجه به مطالب و روشهای گفته شده در بالا -به ویژوال استودیو اجازه دهید بستههای NuGet را در صورت لزوم احیا کند- یا -فعال سازی NuGet Package Restore برای پروژهها- packageها را دریافت کند. بعد از انجام این کار فولدر packages و فایل repositories.config در local هر کاربر ایجاد میشوند.
توجه شود این فولدر و محتویات آن از طریق Add Item to folder دوباره به سورس کنترل اضافه نشود.
2- یا میتوانید ابتدا محتویات فولدر packages به غیر از فایل repositories.config را از سورس کنترل پاک کنید (فولدرها و فایلهای package ها ). به فایل repositories.config برای مسیر فایلهای packages.config هر پروژه نیاز داریم .
شما فولدر packages و فایل repositories.config را checkin کنید (در فولدر packages جز فایل repositories.config فایل یا فولدر دیگری وجود نداشته باشد ).
و در هر بار Build کردن موجود بودن فایلهای package دوباره چک میشوند و اگر موجود نباشند، دریافت میشوند.
نکته مهم این است که اگر Build definition تعریف کرده باشید نیاز به تنظیمات در سرور build برای دریافت packageها دارید.
با تشکر بابت مقاله مفیدی که منتشر کردید.
به معنای کوئری زدن در هر درخواست از دیتابیس برای رفرش کردن نقشهای کاربر نیز میباشد. حال اگر سیستم بزرگ بوده و علاوه بر گروههای کاربری ، دارای سیستم دسترسیها داینامیک هم باشد امکان دارد زمان گزارش گیری کمی افزایش یابد و با تعداد زیاد کاربران این عمل به صرفه نخواهد بود.
خودم هم قصد داشتم مطلبی درباره موضوع مرتبط به این مقاله (SecurityStamp) منتشر کنم که الان با توضیحات کامل شما دیگه لزومی نمیبینم این کار را انجام دهم.
فقط باید توجه داشت که مقدار دهی Interval با
TimeSpan.FromMinutes(0)
کاری که خودم در پروژه <<طراحی فریمورکی برای کار با Asp.net MVC و EF >> انجام دادم به این صورت است که یک فیلد به نام IsChangedPermissions در کلاس کاربر قرار دادم تا هر وقت دسترسیها او تغییر کند این فیلد را با مقدار true مقدار دهی کنم و با این صورت لازم نیست در هر درخواست دسترسیهای کاربر از دیتابیس واکشی شوند . و اگر لازم بود اکانت کاربر را به صورت آنی غیر فعال کنیم کافیست فیلد SecurityStamp او را با متد یاد شده در مطلب تغییر دهیم که این امر با توجه به مقدار دهی interval با مقدار 0 ، سبب خروج کاربر مورد نظر از حساب خود خواهد شد.
البته لازم است بعد از چک کردن فیلد IsChangedPermissions ، اگر مقدار true را در برداشت آن را false مقدار دهی کنیم تا برای درخواستهای بعدی مشکلی پیش نیاید.
برای این منظور یک SecurityStampValidator شخصی سازی شده در نظر گرفتم که قسمت مد نظر برای تغییر به صورت زیر است:
if (validate) { var manager = context.OwinContext.GetUserManager<ApplicationUserManager>(); var userId = getUserIdCallback(context.Identity); if (manager != null) { var user = await manager.FindByIdAsync(userId).WithCurrentCulture(); var reject = true; // Refresh the identity if the stamp matches, otherwise reject if (user != null && manager.SupportsUserSecurityStamp) { var securityStamp = context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType); if (securityStamp == await manager.GetSecurityStampAsync(userId).WithCurrentCulture()) { reject = false; // Regenerate fresh claims if possible and resign in if (user.IsChangedPermissions && regenerateIdentityCallback != null) { var identity = await regenerateIdentityCallback.Invoke(manager, user).WithCurrentCulture();
باید از از حالت INSTEAD OF استفاده کنیم در DML Trigger ای که قراره نوشته بشه.
میتوانیم در یک جدول از دیتابیس مان بر اساس یک شرط خاص, عملیات Insert,Delete,Update را مدیریت کنیم.
بعنوان مثال در قطعه کد زیر ما قبل از عملیات Insert در جدول tblTest چک میکنیم که اگر مقدار ستون FirstName برابر با null بود عملیات Insert آن رکورد در دیتابیس لغو شود.
از دو طریق میتوان به مقادیر فیلدهای رکورد جاری دسترس داشت:
1- استفاده از OBJECT_ID و ذکر نام فیلد مورد نظر
2- گرفتن فیلد مورد نظر از جدول INSERTED یا DELETED
DML Triggerها دارای دو جدول خاص بنامهای INSERTED و DELETED هستند که توسط خود SQL Server مدیریت میشوند.در حقیقت در پشت صحنه, ما با این دو جدول در هنگام تغییر مقادیر دادههای جداول دیتابیس کار میکنیم و نمیتوانیم بصورت مستقیم دادههای جداول موجود در دیتا بیس مان را تغییر دهیم.
جدول INSERTED و DELETED حاوی رکورد جاری است که تحت تاثیر عمل درج, ویرایش و حذف در دیتابیس قرار گرفته است.
اطلاعات بیشتر در اینجا و اینجا
میتوانیم در یک جدول از دیتابیس مان بر اساس یک شرط خاص, عملیات Insert,Delete,Update را مدیریت کنیم.
بعنوان مثال در قطعه کد زیر ما قبل از عملیات Insert در جدول tblTest چک میکنیم که اگر مقدار ستون FirstName برابر با null بود عملیات Insert آن رکورد در دیتابیس لغو شود.
ALTER TRIGGER [dbo].[Prevent_Befor_Insert_Null] ON [dbTest].[dbo].[tblTest] INSTEAD OF INSERT AS BEGIN SET NOCOUNT ON IF OBJECT_ID(N'dbTest.dbo.tblTest.FirstName') is null BEGIN DECLARE @Id int SET @Id = (select Id from inserted) RAISERROR ('مقدار فیلد نام نباید خالی باشد',16,1) ROLLBACK END END
از دو طریق میتوان به مقادیر فیلدهای رکورد جاری دسترس داشت:
1- استفاده از OBJECT_ID و ذکر نام فیلد مورد نظر
2- گرفتن فیلد مورد نظر از جدول INSERTED یا DELETED
DML Triggerها دارای دو جدول خاص بنامهای INSERTED و DELETED هستند که توسط خود SQL Server مدیریت میشوند.در حقیقت در پشت صحنه, ما با این دو جدول در هنگام تغییر مقادیر دادههای جداول دیتابیس کار میکنیم و نمیتوانیم بصورت مستقیم دادههای جداول موجود در دیتا بیس مان را تغییر دهیم.
جدول INSERTED و DELETED حاوی رکورد جاری است که تحت تاثیر عمل درج, ویرایش و حذف در دیتابیس قرار گرفته است.
اطلاعات بیشتر در اینجا و اینجا
۱- متد IsActionAuthorized نام کامل متدی که قرار است اجرا شود را به عنوان پارامتر گرفته و در دیتابیس (در این پیاده سازی به وسیلهی EntityFramework) چک میکند که کاربری که Id اش در AuthManager. AuditUserId است (یعنی کاربری که درخواست اجرای متد را داده است) اجازه اجرای این متد را دارد یا نه. بسته به نیازمندی برنامه شما این دسترسی میتواند به طور ساده فقط مستقیما برای کاربر ثبت شود و یا ترکیبی از دسترسی خود کاربر و دسترسی گروه هایی که این کاربر در آن عضویت دارد باشد.
۲- EFAuthorizationManager کلاس ساده ایست
namespace Framework.ServiceLayer.UserManager { public class EFAuthorizationManager : IAuthorizationManager { public String AuditUserId { get; set; } IUnitOfWork _uow; public EFAuthorizationManager(IUnitOfWork uow) { _uow = uow; } public bool IsActionAuthorized(string actionName) { var res = _uow.Set<User>() .Any(u => u.Id == AuditUserId && u.AllowedActions.Any(a => a.Name == actionName)); return res; } public bool IsPageAuthorized(string pageURL) { //TODO: بررسی وجود دسترسی باید پیاده سازی شود //فقط برای تست return true; } } }
:خلاصه ای از کلاسهای مدل مرتبط را هم در زیر مشاهده میکنید
namespace Framework.DataModel { public class User : BaseEntity { public string UserName { get; set; } public string Password { get; set; } //... [Display(Name = "عملیات مجاز")] public virtual ICollection<Action> AllowedActions { get; set; } } public class Action:BaseEntity { public string Name { get; set; } public Entity RelatedEntity { get; set; } //... public virtual ICollection<User> AllowedUsers { get; set; } } public abstract class BaseEntity { [Key] public int Id { get; set; } //... } }
نظرات مطالب
آموزش Prism #3
سلام . دستتون درد نکنه اقای پاکدل . فقط یه چیزی!
یکی اینکه این اموزشتونو اگه میشه یکم سریعتر بدید . اون روش قبلیه که گفتید رو من خوندم خیلی واضحتر توضیح داده بودین . اما از این یکی زیاد نمیتونم درکش کنم.
اگه میشه لطفا رو یه ساختار کنین . یعنی مثلا همین Prism رو با همون الگویه MVVM ای که داره تویه WPF بگین که ما هم بتونیم استفاده کنیم . شما یکی شو با یه روش، یکی دیگشو با یه روشه دیگه و باز اینارو هر کدوم یکی تو Silver و اون یکی تو WPF . این نظر منه . اگه شما یه دونشونو انتخاب کنید و همینطوری ادامه بدین بهتره که ما هم بتونیم برای خودمون یه جمع بندی و یه راه مشخص پیدا کنیم . سایت واقعا عالی دارین . خیلی چیزا من از این سایت یاد گرفتم . این ماژولار بودن تو این سبک و تا این سطح خیلی برام کاربردی و مهمه . میخوام پایه پروژههای شرکتو بر همین روال قرار بدم . اگه میشد شما از همین Prism و این MEF یه پروژه WPF بسازین فقط یکی دوتا ماژول ساده براش پیاده سازه کنین و یه فیلم بگیرین خیلی ممنون میشم . میخوام ا این روش استفاده کنم اما روال کار برام مبهمه . اگه کتاب یا سری آموزشی در این باره هم دارین بزارین ما استفاده کنیم . اموزش هاتونم من هر روز میام میخونم و چک میکنم اما خیلی دیر دیر مطلب میزارین . حتما این اموزشو ادامه بدین . مخصوصا Prism With MEF In WPF . خلییی باحالین....
یکی اینکه این اموزشتونو اگه میشه یکم سریعتر بدید . اون روش قبلیه که گفتید رو من خوندم خیلی واضحتر توضیح داده بودین . اما از این یکی زیاد نمیتونم درکش کنم.
اگه میشه لطفا رو یه ساختار کنین . یعنی مثلا همین Prism رو با همون الگویه MVVM ای که داره تویه WPF بگین که ما هم بتونیم استفاده کنیم . شما یکی شو با یه روش، یکی دیگشو با یه روشه دیگه و باز اینارو هر کدوم یکی تو Silver و اون یکی تو WPF . این نظر منه . اگه شما یه دونشونو انتخاب کنید و همینطوری ادامه بدین بهتره که ما هم بتونیم برای خودمون یه جمع بندی و یه راه مشخص پیدا کنیم . سایت واقعا عالی دارین . خیلی چیزا من از این سایت یاد گرفتم . این ماژولار بودن تو این سبک و تا این سطح خیلی برام کاربردی و مهمه . میخوام پایه پروژههای شرکتو بر همین روال قرار بدم . اگه میشد شما از همین Prism و این MEF یه پروژه WPF بسازین فقط یکی دوتا ماژول ساده براش پیاده سازه کنین و یه فیلم بگیرین خیلی ممنون میشم . میخوام ا این روش استفاده کنم اما روال کار برام مبهمه . اگه کتاب یا سری آموزشی در این باره هم دارین بزارین ما استفاده کنیم . اموزش هاتونم من هر روز میام میخونم و چک میکنم اما خیلی دیر دیر مطلب میزارین . حتما این اموزشو ادامه بدین . مخصوصا Prism With MEF In WPF . خلییی باحالین....