我知道ServiceStack提供了一个RequiredRole属性来控制权限,但是,这并不完全适用于我的用例。我的网站有很多用户生成的内容。用户只能编辑他们具有显式权限的文档。每个对象或对象组控制权限。因此,如果用户是某个组的管理员,那么他们可以编辑该组管理的所有文档。
在per object per user
的基础上控制对请求的访问的最佳设计模式是什么?我希望尽可能使用DRY方法,因为它会影响我所有API端点的95%。
此外,这可以与FluentValidation集成并返回适当的HTTP响应吗?
非常感谢,
理查德。
答案 0 :(得分:22)
我在ServiceStack应用程序中使用每个对象的权限。实际上,这是一个访问控制列表(ACL)。
我创建了一个Working Self Hosted Console Example,你可以在GitHub上分叉。
我使用下图所示的数据库结构,我的数据库中的资源,如文档,文件,联系人等(我要保护的任何资源)都被赋予了{{1} } id。
权限表包含适用于特定用户,特定组,特定对象和特定对象类型的规则,并且可以灵活地以组合方式接受它们,其中ObjectType
值将被视为通配符。
我发现处理它们的最简单方法是使用请求过滤器属性。通过我的解决方案,我只需在请求路由声明中添加几个属性:
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
}
}
,这将执行检查,以确定请求DTO RequirePermission
的当前用户有权访问DocumentRequest
Document
个对象由属性ObjectId
给出。这就是为了检查我的路线,所以它非常干。
Id
请求过滤器属性:在到达服务的操作方法之前,在过滤器属性中完成测试权限的工作。它具有最低优先级,这意味着它将在验证过滤器之前运行。
此方法将获取活动会话,自定义会话类型(详情如下),它提供活动用户的ID和允许他们访问的组ID。它还将确定来自请求的objectId (如果有)。
它通过检查请求DTO的属性来确定对象ID,以查找具有RequirePermission
属性的值。
使用该信息,它将查询权限来源以找到最合适的权限。
[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'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; } }
}
。类似地,特定请求对象的权限优先于该对象类型的一般权限。
用户特定权限优先于组权限。这意味着,可以通过组权限授予用户访问权限,但在用户级别拒绝用户访问,反之亦然。
如果用户属于允许他们访问资源的组以及拒绝他们访问的其他组,则该用户将具有访问权限。
默认规则是拒绝访问。
在上面的示例代码中,我使用此linq查询来确定用户是否具有权限。该示例使用模拟数据库,您需要将其替换为您自己的提供程序。
UserId == null
我使用自定义会话对象来存储组成员资格,这些会员资格被查询并在用户通过身份验证时添加到会话中。
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();
我希望你觉得这个例子很有用。如果有什么不清楚,请告诉我。
此外,这可以与FluentValidation集成并返回适当的HTTP响应吗?
您不应该在验证处理程序中尝试执行此操作,因为它不是验证。检查您是否拥有权限是一个验证过程。如果您需要根据数据源中的特定值检查某些内容,则不再执行验证。 See this other answer of mine也涵盖了这一点。