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

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

Давайте представим, что у меня есть несколько продуктов и приложение 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");
    }
  }
}

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

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

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

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

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

Просто чтобы добавить немного деталей здесь. Проблема, с которой я столкнулся, заключается в том, что я хочу, чтобы бизнес-правило «Только пользователи с разрешениями могли редактировать продукты» содержалось в одном и только одном месте. Я чувствую, что тот же код, который определяет, может ли пользователь получить или POST для действия Edit, также должен отвечать за определение, следует ли отображать ссылку «Edit» в представлениях Index или Details. Может быть, это невозможно / не возможно, но я чувствую, что это должно быть ...

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

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

Ответы [ 8 ]

29 голосов
/ 26 августа 2009

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

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

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

Альтернативой безопасности на основе ролей является безопасность на основе 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 для вдохновения.

16 голосов
/ 27 августа 2009

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

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

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

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

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

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

Если вы используете FormsAuthentication и пользовательский IUserToken, тогда вы можете создать обертку вокруг 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.

Используя этот метод, вы можете использовать следующее, если хотите, в своем View (где 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 голосов
/ 11 сентября 2009

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

1 голос
/ 14 сентября 2009

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

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

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

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

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

1 голос
/ 27 августа 2009

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

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

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

0 голосов
/ 16 ноября 2011

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

0 голосов
/ 26 августа 2009

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

0 голосов
/ 26 августа 2009

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...