Авторизация на основе ресурсов в .net
Скажем, что у вас есть .net web api с действием GetResource (int resourceId). Это действие (с указанным идентификатором) должно быть разрешено только для пользователя, связанного с этим идентификатором (например, ресурс может быть блогером, написанным пользователем).
Это можно решить разными способами, но ниже приведен пример.
public Resource GetResource(int id)
{
string name = Thread.CurrentPrincipal.Identity.Name;
var user = userRepository.SingleOrDefault(x => x.UserName == name);
var resource = resourceRepository.Find(id);
if (resource.UserId != user.UserId)
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
return resource;
}
где пользователь был аутентифицирован каким-то механиком.
Теперь скажем, что я также хочу, чтобы пользователь, например, типа администратора, имел право потреблять конечную точку (с тем же идентификатором). Этот пользователь не имеет прямого отношения к ресурсу, но имеет авторизацию из-за его типа (или роли). Это можно решить, просто проверив, имеет ли пользователь тип администратора и возвращает ресурс.
Есть ли способ централизовать это таким образом, чтобы мне не приходилось писать код авторизации в каждом действии?
Edit
Основываясь на ответах, я думаю, что должен уточнить свой вопрос.
То, что я действительно делаю, - это механизм, который позволяет иметь авторизацию на основе ресурсов, но в то же время позволяет некоторым пользователям также использовать одну и ту же конечную точку и тот же ресурс. Ниже приведенное действие разрешит это для этой конкретной конечной точки и для этой конкретной роли (Admin).
public Resource GetResource(int id)
{
string name = Thread.CurrentPrincipal.Identity.Name;
var user = userRepository.SingleOrDefault(x => x.UserName == name);
var resource = resourceRepository.Find(id);
if (!user.Roles.Any(x => x.RoleName == "Admin" || resource.UserId != user.UserId)
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
return resource;
}
То, что я получаю, - это общий способ решить эту проблему, так что мне не нужно писать две разные конечные точки с той же целью или писать код, специфичный для ресурсов на каждой конечной точке.
Ответы
Ответ 1
Для авторизации на основе ресурсов я бы предложил использовать идентификатор, основанный на требованиях, и вставить идентификатор пользователя в качестве претензии. Напишите метод расширения, чтобы прочитать заявку от личности. Таким образом, пример кода будет выглядеть следующим образом:
public Resource GetResource(int id)
{
var resource = resourceRepository.Find(id);
if (resource.UserId != User.Identity.GetUserId())
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
return resource;
}
Если вы хотите еще больше упростить код, вы можете написать UserRepository, который знает данные пользователя и репозиторий ресурсов для централизации кода. Код будет выглядеть так:
public Resource GetResource(int id)
{
return User.Identity.GetUserRepository().FindResource(id);
}
Для авторизации на основе роли AuthorizeAttribute будет лучшим местом для ее обработки, и вам лучше использовать отдельное действие или контроллер для этого.
[Authorize(Roles = "admin")]
public Resource GetResourceByAdmin(int id)
{
return resourceRepository.Find(id);
}
[Изменить]
Если OP хочет использовать одно действие для работы с разными типами пользователей, я лично предпочитаю использовать репозиторий пользователя factory. Код действия:
public Resource GetResource(int id)
{
return User.GetUserRepository().FindResource(id);
}
Метод расширения будет:
public static IUserRepository GetUserRepository(this IPrincipal principal)
{
var resourceRepository = new ResourceRepository();
bool isAdmin = principal.IsInRole("Admin");
if (isAdmin)
{
return new AdminRespository(resourceRepository);
}
else
{
return new UserRepository(principal.Identity, resourceRepository);
}
}
Причина, по которой я не хочу использовать AuthorizeAttribute для проверки подлинности каждого ресурса, заключается в том, что у разных ресурсов может быть другой код для проверки права собственности, трудно централизовать код в одном атрибуте, и для этого требуются дополнительные операции с БД, что на самом деле не является необходимо.
Еще одна проблема заключается в том, что AuthroizeAttribute происходит до привязки параметров, поэтому вам нужно убедиться, что параметр действия поступает из данных маршрута. В противном случае, например, из тела сообщения вы не сможете получить значение параметра.
Ответ 2
Я бы посмотрел на реализацию пользовательского System.Web.Http.AuthorizeAttribute, который вы могли бы применить к действиям, которые нуждаются в этом конкретном правиле авторизации. В пользовательской авторизации вы можете разрешить доступ, если пользователь является членом группы "Админы", или если он является автором ресурса.
ИЗМЕНИТЬ:
Основываясь на редактировании OP, позвольте мне расширить то, что я говорю. Если вы переопределите AuthorizeAttribute, вы можете добавить логику, например:
public class AuthorizeAdminsAndAuthors : System.Web.Http.AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
return currentUser.IsInRole("Admins") || IsCurrentUserAuthorOfPost(actionContext);
}
private bool IsCurrentUserAuthorOfPost(HttpActionContext actionContext)
{
// Get id for resource from actionContext
// look up if user is author of this post
return true;
}
Это псевдокод, но должен передать идею. Если у вас есть один атрибут AuthorizeAttribute, который определяет авторизацию на основе ваших требований: текущий запрос - либо от автора сообщения, либо от администратора, то вы можете применить атрибут AuthorizeAdminsAndAuthors к любому ресурсу, где требуется этот уровень авторизации. Таким образом, ваш ресурс будет выглядеть так:
[AuthorizeAdminsAndAuthors]
public Resource GetResource(int id)
{
var resource = resourceRepository.Find(id);
return resource;
}
Ответ 3
Вам необходимо выполнить экстернализацию своей авторизации. Вы хотите переместить всю логику авторизации на отдельный уровень или службу.
Существует несколько рамок - на разных языках - которые позволяют вам это делать. В мире .NET, как предложено в других ответах, у вас есть авторизация на основе требований. У Microsoft есть отличная статья о здесь.
Я бы рекомендовал вам пойти на стандартизованный подход, а именно XACML, расширяемый язык разметки контроля доступа. XACML дает вам 3 вещи:
- стандартная архитектура с понятием точки принятия решения о политике (PDP - это ваша служба авторизации), которая может служить да/нет решений
- стандартный язык для выражения логики авторизации с использованием любого количества параметров/атрибутов, включая атрибуты пользователя и информацию о ресурсе.
- схема запроса/ответа для отправки ваших вопросов авторизации в PDP.
Если мы перейдем к вашему примеру, у вас будет что-то по строкам:
public Resource GetResource(int id)
{
var resource = resourceRepository.Find(id);
if (isAuthorized(User.Identity,resource))
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
return resource;
}
public bool isAuthorized(User u, Resource r){
// Create XACML request here
// Call out to PDP
// return boolean decision
}
Ваш PDP будет содержать следующие правила:
- пользователь может выполнить действие == представление на ресурсе тогда и только тогда, когда resource.owner == user.id
- пользователь с ролью == administrator может выполнить действие == на ресурсе.
Преимущество XACML заключается в том, что вы можете вырабатывать свои правила/логику авторизации независимо от вашего кода. Это означает, что вам не нужно касаться вашего кода приложения всякий раз, когда изменяется логика. XACML также может обслуживать больше параметров/атрибутов - например, идентификатор устройства, IP-адрес, время суток... Наконец, XACML не относится к .NET. Он работает для разных рамок.
Вы можете читать XACML здесь и самостоятельно блог где я пишу об авторизации. Википедия также имеет достойную страницу по этой теме.
Ответ 4
Также рассмотрите подход авторизации, основанный на требованиях, включенный, начиная с .NET 4.5.
http://leastprivilege.com/2012/10/26/using-claims-based-authorization-in-mvc-and-web-api/