Подтвердить что ты не робот

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

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

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

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

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

public interface IProductRepository
{
  IEnumberable<Product> GetProducts();
  ....
}

Также мой репозиторий описывает список пользователей и какие продукты они являются менеджерами для (многие-многие между пользователями и продуктами). В другом месте приложения 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). Вам придется копировать-вставлять несколько раз.
  • Это не очень легко проверить - вы должны макетировать моим счетом четыре объекта для каждого теста.
  • На самом деле не похоже, что "задание" контроллера проверяет, разрешено ли пользователю выполнять действие. Я бы предпочел более гибкое (например, AOP через атрибуты) решение. Однако, это обязательно означает, что вам придется дважды выбрать продукт (один раз в AuthorizationFilter и снова в контроллере)?
  • Было бы лучше вернуть 403, если пользователю не разрешено делать этот запрос? Если да, то как мне это сделать?

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

Спасибо заранее!

Edit

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

Изменить 2

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

bool canEdit = permission.IsGrantedTo(user);

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

permission.Demand(user);

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

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

public class Product
{
    public IEnumerable<ProductAccessRule> 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 для вдохновения.

Ответ 2

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

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

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

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

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

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) { %>
  <a href="/Products/1/Edit">Edit</a>
<% } %>

или в вашем контроллере

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

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

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

Ответ 3

Решения для копирования пасты действительно становятся утомительными через некоторое время, и это действительно раздражает для поддержания. Я бы, вероятно, пошел с настраиваемым атрибутом, делая то, что вам нужно. Вы можете использовать отличный .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, чтобы вы могли получить его в контроллере или сохранить в кеше.

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

Ответ 4

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

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

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

Ответ 5

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

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

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

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

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

Ответ 6

Отвечая на мой собственный вопрос (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 для операции удаления), хотя это технически отдельное деловое правило).

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

Ответ 7

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

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, чтобы вернуть пользовательское разрешение для данного продукта. С использованием перечисления Permission (я думаю, у меня есть правильный синтаксис...) и сравнения разрешений с <, Admin разрешения подразумевают разрешения доступа, что почти всегда правильно.

Ответ 8

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