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