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上进行分叉.
我使用下图所示的数据库结构,我的数据库中的资源,如文档,文件,联系人等(我想要保护的任何资源)都被赋予了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响应?
您不应该尝试在验证处理程序中执行此操作,因为它不是验证.检查您是否拥有权限是一个验证过程.如果您需要根据数据源中的特定值检查某些内容,则不再执行验证.看到我的另一个答案也涵盖了这一点.