Контроль доступа в ASP.NET MVC в зависимости от входных параметров / уровня обслуживания?

Преамбула: это немного философский вопрос. Я больше ищу «правильный» способ сделать это, а не «способ» сделать это.

Представим себе, что у меня есть некоторые продукты, и приложение ASP.NET MVC, выполняющее CRUD для этих продуктов:

mysite.example/products/1 mysite.example/products/1/edit 

Я использую шаблон репозитория, поэтому не имеет значения, откуда эти продукты:

 public interface IProductRepository { IEnumberable GetProducts(); .... } 

Также мой repository описывает список пользователей и какие продукты они являются менеджерами для (многие-многие между пользователями и продуктами). В другом месте приложения Super-Admin выполняет CRUD для пользователей и управляет отношениями между Пользователями и Продуктами, которым им разрешено управлять.

Любому разрешено просматривать любой продукт, но только пользователям, которые обозначены как «админы» для определенного продукта, разрешено вызывать, например, действие «Изменить».

Как мне следует реализовать это в ASP.NET MVC? Если я что-то пропустил, я не могу использовать встроенный атрибут авторизации ASP.NET, поскольку сначала мне понадобилась бы другая роль для каждого продукта, а во-вторых, я не буду знать, какую роль проверить, пока не найду извлек мой Продукт из репозитория.

Очевидно, что вы можете обобщить этот сценарий на большинство сценариев управления контентом – например, пользователям разрешено редактировать собственные сообщения Форума. Пользователям StackOverflow разрешено редактировать собственные вопросы – если у них нет 2000 или более сообщений …

Простейшим решением, например, было бы что-то вроде:

 public class ProductsController { public ActionResult Edit(int id) { Product p = ProductRepository.GetProductById(id); User u = UserService.GetUser(); // Gets the currently logged in user if (ProductAdminService.UserIsAdminForProduct(u, p)) { return View(p); } else { return RedirectToAction("AccessDenied"); } } } 

Мои проблемы:

  • Некоторым из этого кода нужно будет повторить – представьте, что в зависимости от отношения User-Products существует несколько операций (Update, Delete, SetStock, Order, CreateOffer). Вам придется копировать-вставить несколько раз.
  • Это не очень легко проверить – вы должны макетировать моим счетом четыре объекта для каждого теста.
  • На самом деле не похоже, что «задание» controllerа проверяется, разрешено ли пользователю выполнять действие. Я бы предпочел более гибкое (например, AOP через атрибуты) решение. Однако, это обязательно означает, что вам нужно будет выбрать продукт дважды (один раз в AuthorizationFilter и снова в controllerе)?
  • Было бы лучше вернуть 403, если пользователю не разрешено делать этот запрос? Если да, то как мне это сделать?

Я, вероятно, продолжу это обновление, поскольку сам получаю идеи, но я очень хочу услышать ваше мнение!

Заранее спасибо!

редактировать

Просто добавьте немного деталей здесь. Проблема, с которой я сталкиваюсь, заключается в том, что я хочу, чтобы бизнес-правило «Только пользователи с разрешением могут редактировать продукты», которые должны содержаться в одном и только одном месте. Я чувствую, что тот же код, который определяет, может ли пользователь выполнить GET или POST для действия «Редактировать», также должен нести ответственность за определение того, нужно ли отображать ссылку «Изменить» в представлениях «Индекс» или «Детали». Возможно, это невозможно / невозможно, но я чувствую, что это должно быть …

Изменить 2

Запуск щедрости на этом. Я получил хорошие и полезные ответы, но ничего, что мне было комфортно «принимать». Имейте в виду, что я ищу хороший чистый метод, чтобы бизнес-логика определяла, будет ли отображаться ссылка «Редактировать» на индексном представлении в том же месте, которое определяет, будет ли запрос к Продуктам / Редактировать / 1 разрешено или нет. Я бы хотел, чтобы загрязнение в моем методе действия достигло абсолютного минимума. В идеале, я ищу решение на основе атрибутов, но я согласен, что это невозможно.

Прежде всего, я думаю, что вы уже на полпути поняли это, потому что вы заявили, что

поскольку сначала мне понадобилась бы другая роль для каждого продукта, а во-вторых, я не буду знать, какую роль проверить, пока я не извлечу свой продукт из репозитория

Я видел, как многие попытки создать защиту на основе ролей делают то, чего она никогда не собиралась делать, но вы уже прошли этот момент, так что это круто 🙂

Альтернативой безопасности на основе ролей является безопасность на основе ACL, и я думаю, что это то, что вам нужно здесь.

Вам все равно необходимо получить ACL для продукта, а затем проверить, имеет ли пользователь право на продукт. Это настолько чувствительно к контексту и тяжелому взаимодействию, что я считаю, что чисто декларативный подход слишком негибкий и слишком неявный (т. Е. Вы не можете понять, сколько чтений базы данных связано с добавлением одного атрибута в некоторый код).

Я думаю, что подобные сценарии лучше всего моделируются classом, который инкапсулирует логику ACL, позволяя вам либо запросить решение, либо сделать утверждение на основе текущего контекста – что-то вроде этого:

 var p = this.ProductRepository.GetProductById(id); var user = this.GetUser(); var permission = new ProductEditPermission(p); 

Если вы просто хотите узнать, может ли пользователь редактировать продукт, вы можете отправить запрос:

 bool canEdit = permission.IsGrantedTo(user); 

Если вы просто хотите, чтобы у пользователя были права на продолжение, вы можете опубликовать утверждение:

 permission.Demand(user); 

Затем это должно быть исключение, если разрешение не предоставлено.

Все это предполагает, что class Product (переменная p ) имеет ассоциированный ACL, например:

 public class Product { public IEnumerable AccessRules { get; } // other members... } 

Вы можете взглянуть на System.Security.AccessControl.FileSystemSecurity для вдохновения в отношении моделирования списков ACL.

Если текущий пользователь совпадает с Thread.CurrentPrincipal (что имеет место в ASP.NET MVC, IIRC), вы можете просто использовать приведенные выше методы разрешения для:

 bool canEdit = permission.IsGranted(); 

или

 permission.Demand(); 

потому что пользователь будет неявным. Вы можете взглянуть на System.Security.Permissions.PrincipalPermission для вдохновения.

Из того, что вы описываете, это похоже на то, что вам нужна определенная форма контроля доступа пользователя, а не разрешения на основе ролей. Если это так, то это должно быть реализовано в вашей бизнес-логике. Ваш сценарий звучит так, как будто вы можете реализовать его на своем уровне обслуживания.

В основном вы должны реализовать все функции в ProductRepository с точки зрения текущего пользователя, а продукты отмечены разрешениями для этого пользователя.

Это звучит сложнее, чем на самом деле. Во-первых, вам нужен пользовательский интерфейс токена, который содержит информацию о пользователе uid и списке ролей (если вы хотите использовать роли). Вы можете использовать IPrincipal или создавать свои собственные

 public interface IUserToken { public int Uid { get; } public bool IsInRole(string role); } 

Затем в вашем controllerе вы разбираете токен пользователя в свой конструктор репозитория.

 IProductRepository ProductRepository = new ProductRepository(User); //using IPrincipal 

Если вы используете FormsAuthentication и пользовательский IUserToken, тогда вы можете создать Wrapper вокруг IPrincipal, чтобы ваш ProductRepository был создан следующим образом:

 IProductRepository ProductRepository = new ProductRepository(new IUserTokenWrapper(User)); 

Теперь все ваши функции IProductRepository должны получить доступ к токену пользователя, чтобы проверить разрешения. Например:

 public Product GetProductById(productId) { Product product = InternalGetProductById(UserToken.uid, productId); if (product == null) { throw new NotAuthorizedException(); } product.CanEdit = ( UserToken.IsInRole("admin") || //user is administrator UserToken.Uid == product.CreatedByID || //user is creator HasUserPermissionToEdit(UserToken.Uid, productId) //other custom permissions ); } 

Если вам интересно узнать список всех продуктов, в коде доступа к данным вы можете запросить запрос на основе разрешения. В вашем случае левое соединение, чтобы увидеть, содержит ли таблица «многие-ко-многим» UserToken.Uid и productId. Если присутствует правая сторона соединения, вы знаете, что пользователь имеет разрешение на этот продукт, а затем вы можете установить свой Product.CanEdit boolean.

Используя этот метод, вы можете использовать следующее, если хотите, в вашем представлении (где Model – ваш продукт).

 <% if(Model.CanEdit) { %> Edit <% } %> 

или в вашем controllerе

 public ActionResult Get(int id) { Product p = ProductRepository.GetProductById(id); if (p.CanEdit) { return View("EditProduct"); } else { return View("Product"); } } 

Преимущество этого метода в том, что безопасность встроена в ваш сервисный уровень (ProductRepository), поэтому он не обрабатывается вашими controllerами и не может быть обойден вашим controllerом.

Главное, что безопасность помещается в вашу бизнес-логику, а не в ваш controller.

Решения для копирования пасты действительно становятся утомительными через некоторое время, и их очень раздражает. Я бы, вероятно, пошел с настраиваемым атрибутом, делая то, что вам нужно. Вы можете использовать отличный .NET Reflector, чтобы увидеть, как реализован AuthorizeAttribute и выполнить свою собственную логику.

Что он делает, это наследование FilterAttribute и реализация IAuthorizationFilter. Я не могу проверить это на данный момент, но что-то вроде этого должно работать.

 [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] public class ProductAuthorizeAttribute : FilterAttribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) { throw new ArgumentNullException("filterContext"); } object productId; if (!filterContext.RouteData.Values.TryGetValue("productId", out productId)) { filterContext.Result = new HttpUnauthorizedResult(); return; } // Fetch product and check for accessrights if (user.IsAuthorizedFor(productId)) { HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache; cache.SetProxyMaxAge(new TimeSpan(0L)); cache.AddValidationCallback(new HttpCacheValidateHandler(this.Validate), null); } else filterContext.Result = new HttpUnauthorizedResult(); } private void Validate(HttpContext context, object data, ref HttpValidationStatus validationStatus) { // The original attribute performs some validation in here as well, not sure it is needed though validationStatus = HttpValidationStatus.Valid; } } 

Возможно, вы также можете сохранить продукт / пользователя, который вы получаете в файле filterContext.Controller.TempData, чтобы вы могли его получить в controllerе или сохранить в кеше.

Изменить: я только что заметил часть ссылки на редактирование. Лучший способ, о котором я могу думать, – разделить часть авторизации от атрибута и сделать HttpHelper для нее, которую вы можете использовать в своем представлении.

Я склонен думать, что авторизация является частью вашей бизнес-логики (или, по крайней мере, вне вашей логики controllerа). Я согласен с kevingessner выше, поскольку проверка авторизации должна быть частью вызова для извлечения элемента. В методе OnException вы можете показать страницу входа (или что-то, что вы настроили в файле web.config) следующим образом:

 if (...) { Response.StatusCode = 401; Response.StatusDescription = "Unauthorized"; HttpContext.Response.End(); } 

И вместо того, чтобы делать вызовы UserRepository.GetUserSomehowFromTheRequest () во всех методах действий, я бы сделал это один раз (например, переопределив метод Controller.OnAuthorization), затем привяжите эти данные где-нибудь в базовом classе controllerа для последующего использования (например, недвижимость).

Я думаю, что это нереально, и нарушение разделения проблем, чтобы ожидать, что controller / модельный код контролируют то, что визуализирует представление. Код controllerа / модели может установить флаг в модели представления, который может использовать вид, чтобы определить, что он должен делать, но я не думаю, что вы должны ожидать, что один метод будет использоваться как controllerом / моделью, так и представлением для управления доступом и рендерингом модели.

Сказав, что вы можете подойти к этому одним из двух способов – оба будут включать в себя модель представления, которая содержит некоторые annotations, используемые представлением в дополнение к реальной модели. В первом случае вы можете использовать атрибут для контроля доступа к действию. Это было бы моим преимуществом, но было бы связано с украшением каждого метода независимо – если все действия в controllerе не имеют одинаковых атрибутов доступа.

Для этой цели я разработал атрибут «роль или владелец». Он проверяет, что пользователь находится в определенной роли или является владельцем данных, создаваемых этим методом. Собственность, в моем случае, контролируется наличием отношения внешнего ключа между пользователем и данными, то есть у вас есть таблица ProductOwner, и должна быть строка, содержащая пару продукта / владельца для продукта и текущего пользователя. Он отличается от обычного AuthorizeAttribute тем, что, когда проверка прав собственности или роли не выполняется, пользователь перенаправляется на страницу с ошибкой, а не на страницу входа. В этом случае каждому методу необходимо установить флаг в модели представления, который указывает, что модель может быть отредактирована.

Кроме того, вы можете реализовать аналогичный код в методах ActionExecuting / ActionExecuted controllerа (или базового controllerа, чтобы он последовательно применялся для всех controllerов). В этом случае вам нужно будет написать код для определения того, какое действие выполняется, поэтому вы знаете, следует ли прервать действие на основании права собственности на данный продукт. Тот же метод установил бы флаг, указывающий, что модель может быть отредактирована. В этом случае вам, вероятно, понадобится иерархия модели, чтобы вы могли использовать модель как редактируемую модель, чтобы вы могли установить свойство независимо от конкретного типа модели.

Этот вариант кажется более связанным со мной, чем с использованием атрибута и, возможно, более сложным. В случае атрибута вы можете его сконструировать так, чтобы он использовал различные имена таблиц и свойств в качестве атрибутов для атрибута и использовал reflection для получения правильных данных из вашего репозитория на основе свойств атрибута.

Отвечая на мой собственный вопрос (eep!), Глава 1 Professional ASP.NET MVC 1.0 (учебник NerdDinner) рекомендует аналогичное решение для моего выше:

 public ActionResult Edit(int id) { Dinner dinner = dinnerRepositor.GetDinner(id); if(!dinner.IsHostedBy(User.Identity.Name)) return View("InvalidOwner"); return View(new DinnerFormViewModel(dinner)); } 

Помимо того, что я проголодался за своим ужином, это на самом деле ничего не добавляет, так как в этом учебном курсе повторяется код, реализующий бизнес-правило, сразу же в соответствующем методе POST-действия и в представлении «Детали» (фактически в дочернем частичном Подробный просмотр)

Это нарушает SRP? Если бизнес-правило изменилось (чтобы, например, любой, у кого был RSVP’d, можно было отредактировать обед), вам придется изменить методы GET и POST, а также методы View (и GET и POST и View для операции удаления) , хотя это технически отдельное правило для бизнеса).

Вытаскивает логику в какой-то предмет арбитража разрешений (как я уже делал выше) так же хорошо, как и получается?

Вы на правильном пути, но вы можете инкапсулировать всю проверку прав на один метод, например GetProductForUser , который принимает продукт, пользователя и требуемое разрешение. Бросив исключение, попавшее в обработчик OnException controllerа, обработка выполняется в одном месте:

 enum Permission { Forbidden = 0, Access = 1, Admin = 2 } public class ProductForbiddenException : Exception { } public class ProductsController { public Product GetProductForUser(int id, User u, Permission perm) { Product p = ProductRepository.GetProductById(id); if (ProductPermissionService.UserPermission(u, p) < perm) { throw new ProductForbiddenException(); } return p; } public ActionResult Edit(int id) { User u = UserRepository.GetUserSomehowFromTheRequest(); Product p = GetProductForUser(id, u, Permission.Admin); return View(p); } public ActionResult View(int id) { User u = UserRepository.GetUserSomehowFromTheRequest(); Product p = GetProductForUser(id, u, Permission.Access); return View(p); } public override void OnException(ExceptionContext filterContext) { if (typeof(filterContext.Exception) == typeof(ProductForbiddenException)) { // handle me! } base.OnException(filterContext); } } 

Вам просто нужно предоставить ProductPermissionService.UserPermission, чтобы вернуть разрешение пользователя на данный продукт. С помощью enums Permission (я думаю, что у меня есть правильный синтаксис ...) и сравнения разрешений с < , разрешениями администратора подразумеваются разрешения доступа, который почти всегда прав.

Вы можете использовать XACML-реализацию. Таким образом, вы можете экпортировать авторизацию, а также иметь repository для своих политик вне вашего кода.

  • Html.DropdownListДля выбранного значения не задано
  • Перенаправление на действие и необходимость передачи данных
  • Как указать различные макеты в файле ViewStart бритвы ASP.NET MVC 3?
  • Как я могу получить базовый URL моего webapp в ASP.NET MVC?
  • MVVM ViewModel против MVC ViewModel
  • Токен-маркер анти-подделки и маркер поля формы не совпадают с MVC 4
  • Использование расширений MVC HtmlHelper из декларативных представлений Razor
  • Как использовать Simple Ajax Beginform в Asp.net MVC 4?
  • Есть ли хороший / правильный способ решения проблемы цикла инъекций зависимостей в учебнике ASP.NET MVC ContactsManager?
  • Поместить контент в объект HttpResponseMessage?
  • Обновление пользовательских данных - Идентификация ASP.NET
  • Давайте будем гением компьютера.