使用ServiceStack控制"每个用户每个对象"权限的最佳设计模式?

ric*_*ver 19 permissions design-patterns servicestack

我知道ServiceStack提供了一个RequiredRole属性来控制权限,但是,这并不完全适用于我的用例.我的网站有很多用户生成的内容.用户只能编辑他们具有显式权限的文档.每个对象或对象组控制权限.因此,如果用户是组的管理员,则他们可以编辑该组管理的所有文档.

在此per object per user基础上控制对请求的访问的最佳设计模式是什么?我希望尽可能使用DRY方法,因为它会影响我所有API端点的95%.

此外,它是否可以与FluentValidation集成并返回适当的HTTP响应?

非常感谢,

理查德.

Sco*_*ott 23

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

我已经创建了一个Working Self Hosted Console示例,您可以在GitHub上进行分叉.

ACL模式:

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

数据库

权限表包含适用于特定用户,特定组,特定对象和特定对象类型的规则,并且可以灵活地以组合形式接受它们,其中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
    }
}
Run Code Online (Sandbox Code Playgroud)

我有一个filter属性调用RequirePermission,这将执行检查以查看请求DTO的当前用户DocumentRequest是否可以访问属性给出的Document对象.这就是为了检查我的路线,所以它非常干燥.ObjectIdId

RequirePermission请求过滤属性:

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

此方法将获取活动会话,即自定义会话类型(下面的详细信息),它提供活动用户的ID以及允许他们访问的组ID.这也将决定OBJECTID 如果任何来自请求.

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

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

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; } }
}
Run Code Online (Sandbox Code Playgroud)

权限优先:

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

  • 与当前用户匹配的权限优先于所有用户的一般权限,即在哪里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();
Run Code Online (Sandbox Code Playgroud)

自定义会话:

我使用了一个自定义会话对象来存储组成员身份,这些是在用户通过身份验证时查找并添加到会话中的.

// 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我希望你觉得这个例子很有用.如果有什么不清楚,请告诉我.

流利的验证:

此外,它是否可以与FluentValidation集成并返回适当的HTTP响应?

您不应该尝试在验证处理程序中执行此操作,因为它不是验证.检查您是否拥有权限是一个验证过程.如果您需要根据数据源中的特定值检查某些内容,则不再执行验证.看到我的另一个答案也涵盖了这一点.