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

Лучший шаблон дизайна для управления разрешениями на основе "для каждого объекта на пользователя" с помощью ServiceStack?

Я знаю, что ServiceStack предоставляет атрибут RequiredRole для управления разрешениями, однако это не полностью работает для моего варианта использования. У меня есть веб-сайт с большим количеством пользовательского контента. Пользователи могут редактировать документы, на которые у них есть явные разрешения. Разрешения контролируются для каждого объекта или группы объектов. Таким образом, если использование является администратором группы, тогда они могут редактировать все документы, управляемые этой группой.

Каков наилучший шаблон дизайна для управления доступом к запросу для каждого объекта на каждого пользователя. Я хочу приблизиться к этому, используя DRY, насколько это возможно, поскольку это затронет 95% всех конечных точек API.

Кроме того, можно ли это интегрировать с FluentValidation и возвращать соответствующие ответы HTTP?

Большое спасибо,

Ричард.

4b9b3361

Ответ 1

Я использую разрешения для каждого объекта в своих приложениях ServiceStack. Эффективно это список контроля доступа (ACL).

Я создал "Рабочий Self Hosted Console" , который можно развить на GitHub.

Рисунок ACL:

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

Database

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

Обеспечение безопасности и маршрутов:

Я считаю, что самый простой способ справиться с ними - использовать атрибут фильтра запроса. С моим решением я просто добавляю пару атрибутов в объявление маршрута запроса:

[RequirePermission(ObjectType.Document)]
[Route("/Documents/{Id}", "GET")]
public class DocumentRequest : IReturn<string>
{
    [ObjectId]
    public int Id { get; set; }
}

[Authenticate]
public class DocumentService : Service
{
    public string Get(DocumentRequest request)
    {
        // We have permission to access this document
    }
}

У меня есть вызов атрибута фильтра RequirePermission, это проверит проверку, чтобы увидеть, что текущий пользователь, запрашивающий DTO DocumentRequest, имеет доступ к объекту Document, чей ObjectId задается свойством Id, Это все, что нужно для проводки проверки на моих маршрутах, поэтому оно очень СУХОЙ.

Атрибут фильтра RequirePermission:

Задача тестирования для разрешения выполняется в атрибуте фильтра, прежде чем обратиться к методу действия службы. Он имеет самый низкий приоритет, что означает, что он будет работать до фильтров проверки.

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

Он определяет идентификатор объекта, исследуя свойства запроса DTO, чтобы найти значение, имеющее атрибут [ObjectId].

С этой информацией он запросит источник разрешения, чтобы найти наиболее подходящее разрешение.

public class RequirePermissionAttribute : Attribute, IHasRequestFilter
{
    readonly int objectType;

    public RequirePermissionAttribute(int objectType)
    {
        // Set the object type
        this.objectType = objectType;
    }

    IHasRequestFilter IHasRequestFilter.Copy()
    {
        return this;
    }

    public void RequestFilter(IRequest req, IResponse res, object requestDto)
    {
        // Get the active user session
        var session = req.GetSession() as MyServiceUserSession;
        if(session == null || session.UserAuthId == 0)
            throw HttpError.Unauthorized("You do not have a valid session");

        // Determine the Id of the requested object, if applicable
        int? objectId = null;
        var property = requestDto.GetType().GetPublicProperties().FirstOrDefault(p=>Attribute.IsDefined(p, typeof(ObjectIdAttribute)));
        if(property != null)
            objectId = property.GetValue(requestDto,null) as int?;

        // You will want to use your database here instead to the Mock database I'm using
        // So resolve it from the container
        // var db = HostContext.TryResolve<IDbConnectionFactory>().OpenDbConnection());
        // You will need to write the equivalent 'hasPermission' query with your provider

        // Get the most appropriate permission
        // The orderby clause ensures that priority is given to object specific permissions first, belonging to the user, then to groups having the permission
        // descending selects int value over null
        var hasPermission = session.IsAdministrator || 
                            (from p in Db.Permissions
                             where p.ObjectType == objectType && ((p.ObjectId == objectId || p.ObjectId == null) && (p.UserId == session.UserAuthId || p.UserId == null) && (session.Groups.Contains(p.GroupId) || p.GroupId == null))
                             orderby p.ObjectId descending, p.UserId descending, p.Permitted, p.GroupId descending
                             select p.Permitted).FirstOrDefault();

        if(!hasPermission)
            throw new HttpError(System.Net.HttpStatusCode.Forbidden, "Forbidden", "You do not have permission to access the requested object");
    }

    public int Priority { get { return int.MinValue; } }
}

Приоритет разрешения:

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

  • Разрешения, соответствующие текущему пользователю, имеют больший приоритет, чем общие разрешения для всех пользователей, а именно UserId == null. Аналогично, разрешение для специально запрошенного объекта имеет более высокий приоритет, чем общее разрешение для этого типа объекта.

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

  • Если пользователь принадлежит группе, которая разрешает им доступ к ресурсу и другой группе, которая отказывает им в доступе, тогда у пользователя будет доступ.

  • Правило по умолчанию - это запретить доступ.

Реализация:

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

session.IsAdministrator || 
(from p in Db.Permissions
 where p.ObjectType == objectType && 
     ((p.ObjectId == objectId || p.ObjectId == null) && 
     (p.UserId == session.UserAuthId || p.UserId == null) &&
     (session.Groups.Contains(p.GroupId) || p.GroupId == null))
 orderby p.ObjectId descending, p.UserId descending, p.Permitted, p.GroupId descending
 select p.Permitted).FirstOrDefault();

Пользовательский сеанс:

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

// Custom session handles adding group membership information to our session
public class MyServiceUserSession : AuthUserSession
{
    public int?[] Groups { get; set; }
    public bool IsAdministrator { get; set; }

    // The int value of our UserId is converted to a string!?! :( by ServiceStack, we want an int
    public new int UserAuthId { 
        get { return base.UserAuthId == null ? 0 : int.Parse(base.UserAuthId); }
        set { base.UserAuthId = value.ToString(); }
    }


    // Helper method to convert the int[] to int?[]
    // Groups needs to allow for null in Contains method check in permissions
    // Never set a member of Groups to null
    static T?[] ConvertArray<T>(T[] array) where T : struct
    {
        T?[] nullableArray = new T?[array.Length];
        for(int i = 0; i < array.Length; i++)
            nullableArray[i] = array[i];
        return nullableArray;
    }

    public override void OnAuthenticated(IServiceBase authService, ServiceStack.Auth.IAuthSession session, ServiceStack.Auth.IAuthTokens tokens, System.Collections.Generic.Dictionary<string, string> authInfo)
    {
        // Determine UserId from the Username that is in the session
        var userId = Db.Users.Where(u => u.Username == session.UserName).Select(u => u.Id).First();

        // Determine the Group Memberships of the User using the UserId
        var groups = Db.GroupMembers.Where(g => g.UserId == userId).Select(g => g.GroupId).ToArray();

        IsAdministrator = groups.Contains(1); // Set IsAdministrator (where 1 is the Id of the Administrator Group)

        Groups = ConvertArray<int>(groups);
        base.OnAuthenticated(authService, this, tokens, authInfo);
    }
}

Надеюсь, вы найдете этот пример полезным. Дайте мне знать, если что-то неясно.

Свободная проверка:

Кроме того, можно ли это интегрировать с FluentValidation и возвращать соответствующие ответы HTTP?

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