控制“每个用户的每个对象"权限的最佳设计模式. ServiceStack的基础? [英] Best design pattern to control permissions on a "per object per user" basis with ServiceStack?

查看:82
本文介绍了控制“每个用户的每个对象"权限的最佳设计模式. ServiceStack的基础?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我知道ServiceStack提供了RequiredRole属性来控制权限,但是,这不适用于我的用例.我有一个网站,其中包含许多用户生成的内容.用户只能编辑他们拥有明确权限的文档.权限是按对象或对象组控制的.因此,如果用户是某个网上论坛的管理员,那么他们可以编辑该网上论坛管理的所有文档.

I know that ServiceStack provides a RequiredRole attribute to control permissions, however, this does not fully work for my use case. I have a website with a lot of user-generated content. Users can only edit documents they have explicit permissions to. Permissions are controlled per object or group of the object. So, if a user is an admin of a group then they can edit all documents managed by that group.

per object per user为基础来控制对请求的访问的最佳设计模式是什么?我想以一种尽可能干燥的方法来解决这个问题,因为它将影响我所有API端点的95%.

What is the best design pattern to control access to a request on a per object per user basis? I want to approach this with as DRY a methodology as possible as it will affect 95% of all my API endpoints.

还可以将它与FluentValidation集成并返回适当的HTTP响应吗?

Also, can this be integrated with FluentValidation and return appropriate HTTP responses?

非常感谢,

理查德.

推荐答案

我在ServiceStack应用程序中使用每个对象的权限.实际上,这是一个访问控制列表(ACL).

I use per object permissions in my ServiceStack applications. Effectively this is an Access-Control-List (ACL).

我创建了一个工作自托管控制台示例,您可以在GitHub上创建该示例.

I have created a Working Self Hosted Console Example which you can fork on GitHub.

我使用下图所示的数据库结构,从而使数据库中的资源(例如文档,文件,联系人等)(我要保护的任何资源)都被赋予ObjectType id

I use the database structure shown in the diagram below, whereby resources in my database such as documents, files, contacts etc (any resource I want to protect) are all given an ObjectType id.

权限表包含适用于特定用户,特定组,特定对象和特定对象类型的规则,并且可以灵活地组合接受它们,其中null值将被视为通配符.

The permissions table contains rules that apply to specific users, specific groups, specific objects and specific object types, and is flexible to accept them in combinations, where a null value will be treated like a wildcard.

我发现处理它们的最简单方法是使用请求过滤器属性.通过我的解决方案,我只需向请求路由声明中添加几个属性:

I find the easiest way to handle them is to use a request filter attribute. With my solution I simply add a couple of attributes to my request route declaration:

[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给出.这就是对我的路线进行检查的全部步骤,所以非常干.

I have a filter attribute call RequirePermission, this will perform the check to see that the current user requesting the DTO DocumentRequest has access to the Document object whose ObjectId is given by the property Id. That's all there is to wiring up the checking on my routes, so it's very DRY.

在进入服务的操作方法之前,先在filter属性中完成权限测试的工作.它的优先级最低,这意味着它将在验证过滤器之前运行.

The job of testing for permission is done in the filter attribute, before reaching the service's action method. It has the lowest priority which means it will run before validation filters.

此方法将获取活动会话,这是自定义会话类型(以下详细信息),其中提供了活动用户的ID和允许他们访问的组ID.还将根据请求确定objectId .

This method will get the active session, a custom session type (details below), which provides the active user's Id and the group Ids they are permitted to access. It will also determine the objectId if any from the request.

它通过检查请求DTO的属性以确定具有[ObjectId]属性的值来确定对象ID.

It determines the object id by examining the request DTO's properties to find the value having the [ObjectId] attribute.

使用该信息,它将查询权限源以找到最合适的权限.

With that information it will query the permission source to find the most appropriate permission.

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's 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; } }
}

权限优先级:

从权限表中读取权限时,将使用最高优先级的权限来确定它们是否具有访问权限.权限条目越具体,则对结果进行排序时其优先级越高.

Permission Priority:

When the permissions are read from the permission table, the highest priority permission is used to determine if they have access. The more specific the permission entry is, the higher the priority it has when the results are ordered.

  • 与当前用户匹配的权限比所有用户(即,UserId == null )的常规权限具有更高的优先级.类似地,对特定请求对象的许可比对该对象类型的一般许可具有更高的优先级.

  • Permissions matching the current user have greater priority than general permissions for all users i.e where UserId == null. Similarly a permission for the specifically requested object has higher priority than the general permission for that object type.

用户特定的权限优先于组权限.这意味着,可以通过组权限向用户授予访问权限,但在用户级别拒绝该用户访问权限,反之亦然.

User specific permissions take precedence over group permissions. This means, that a user can be granted access by a group permission but be denied access at user level, or vice versa.

如果用户属于允许他们访问资源的组,而属于拒绝访问该用户的另一个组,则该用户将具有访问权限.

Where the user belongs to a group that allows them access to a resource and to another group that denies them access, then the user will have access.

默认规则是拒绝访问.

在上面的示例代码中,我已使用此linq查询来确定用户是否具有权限.该示例使用模拟的数据库,您将需要用自己的提供程序替换它.

In my example code above I have used this linq query to determine if the user has permission. The example uses a mocked database, and you will need to substitute it with your own provider.

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:

I have used a custom session object to store the group memberships, these are looked up and added to the session when the user is authenticated.

// 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);
    }
}

我希望您发现此示例有用.让我知道是否有任何不清楚的地方.

I hope you find this example useful. Let me know if anything is unclear.

还可以将其与FluentValidation集成并返回适当的HTTP响应吗?

Also, can this be integrated with FluentValidation and return appropriate HTTP responses?

您不应尝试在验证处理程序中执行此操作,因为这不是验证.检查您是否具有权限是一个验证过程.如果您需要对照数据源中的特定值检查某项内容,那么您将不再执行验证. 查看我的其他答案,其中也对此进行了介绍.

You shouldn't try and do this in the validation handler, because it is not validation. Checking if you have permission is a verification process. If you require to check something against a specific value in a datasource you are no longer performing validation. See this other answer of mine which also covers this.

这篇关于控制“每个用户的每个对象"权限的最佳设计模式. ServiceStack的基础?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆