Web API每个实体的OData安全性
背景:
我有一个非常大的OData模型,目前使用WCF数据服务(OData)来公开它。 不过,微软已经表示WCF数据服务已经死亡 ,Web API OData是他们将要进行的方式。
所以我正在研究如何使Web API OData和WCF数据服务一样工作。
问题设置:
模型的某些部分不需要保护,但有一部分可以。 例如,客户列表需要安全来限制谁可以阅读,但是我有其他列表,比如产品列表,任何人都可以查看。
Customers实体有许多可以达到的关联。 如果您计算2+级关联,则可以通过数百种方式(通过关联)访问客户。 例如Prodcuts.First().Orders.First().Customer
。 由于客户是我系统的核心,您可以从大多数任何实体开始,最终将您的方式与客户列表联系起来。
WCF数据服务有一种方法可以让我通过这样的方法把安全性放在一个特定的实体上:
[QueryInterceptor("Customers")] public Expression<Func<Customer, bool>> CheckCustomerAccess() { return DoesCurrentUserHaveAccessToCustomers(); }
当我看Web API的OData,我没有看到这样的事情。 另外我非常担心,因为我所做的控制器似乎并没有被召唤,当一个协会是遵循的。 (这意味着我不能把安全性放在CustomersController
。)
我担心,我将不得不设法列举协会可以如何得到客户,并在每一个方面安全的方式。
题:
有没有一种方法可以将安全性放在Web API OData的特定实体上? (没有必要列举所有可能以某种方式扩展到该实体的关联)
更新 :在这个时候,我会build议您遵循疫苗公布的解决scheme,这是基于OData团队的意见。
你需要做的是创build一个新的属性,从EnableQueryAttributeinheritanceOData 4(或QuerableAttribute,取决于你正在与哪个版本的Web API \ OData交谈),并重写ValidateQuery(与从QuerableAttributeinheritance的方法相同)检查是否存在合适的SelectExpand属性。
要设置一个新的项目来testing这个做到以下几点:
- 使用Web API 2创build一个新的ASP.Net项目
- 创build您的entity framework数据上下文。
- 添加一个新的“Web API 2 OData控制器…”控制器。
- 在WebApiConfigRegister(…)方法中添加如下:
码:
ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Customer>("Customers"); builder.EntitySet<Order>("Orders"); builder.EntitySet<OrderDetail>("OrderDetails"); config.Routes.MapODataServiceRoute("odata", "odata", builder.GetEdmModel()); //config.AddODataQueryFilter(); config.AddODataQueryFilter(new SecureAccessAttribute());
在上面,Customer,Order和OrderDetail是我的entity framework实体。 config.AddODataQueryFilter(新的SecureAccessAttribute())注册我的SecureAccessAttribute以供使用。
- SecureAccessAttribute的实现如下:
码:
public class SecureAccessAttribute : EnableQueryAttribute { public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions) { if(queryOptions.SelectExpand != null && queryOptions.SelectExpand.RawExpand != null && queryOptions.SelectExpand.RawExpand.Contains("Orders")) { //Check here if user is allowed to view orders. throw new InvalidOperationException(); } base.ValidateQuery(request, queryOptions); } }
请注意,我允许访问客户控制器,但我限制对订单的访问。 我已经实施的唯一的控制器是以下的一个:
public class CustomersController : ODataController { private Entities db = new Entities(); [SecureAccess(MaxExpansionDepth=2)] public IQueryable<Customer> GetCustomers() { return db.Customers; } // GET: odata/Customers(5) [EnableQuery] public SingleResult<Customer> GetCustomer([FromODataUri] int key) { return SingleResult.Create(db.Customers.Where(customer => customer.Id == key)); } }
- 在要保护的所有操作中应用该属性。 它和EnableQueryAttribute完全一样。 可以在这里find一个完整的示例(包括Nuget包终结一切,使这个50Mb下载): http : //1drv.ms/1zRmmVj
我只想对其他一些解决scheme做一些评论:
- Leyenda的解决scheme不是简单的,因为它是相反的方式,但是否则是超级密切! 事实上,构build器将在entity framework中寻找扩展属性,并不会打击客户控制器! 我什至没有一个,如果你删除安全属性,它仍然会检索订单就好了,如果您将扩展命令添加到您的查询。
- 设置模型构build器将禁止访问您在全球和所有人中删除的实体,所以这不是一个好的解决scheme。
- 冯钊的解决scheme可以工作,但是在任何地方,您都必须手动删除您想要保护的项目,这不是一个好的解决scheme。
当我问Web API OData团队时,我得到了这个答案。 这似乎与我接受的答案非常相似,但它使用了IAuthorizationFilter。
为了完整性,我想我会在这里发表:
对于实体集或导航属性出现在path中,我们可以定义一个消息处理程序或一个授权filter,并在那里检查用户请求的目标实体集。 例如,一些代码片段:
public class CustomAuthorizationFilter : IAuthorizationFilter { public bool AllowMultiple { get { return false; } } public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync( HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation) { // check the auth var request = actionContext.Request; var odataPath = request.ODataProperties().Path; if (odataPath != null && odataPath.NavigationSource != null && odataPath.NavigationSource.Name == "Products") { // only allow admin access IEnumerable<string> users; request.Headers.TryGetValues("user", out users); if (users == null || users.FirstOrDefault() != "admin") { throw new HttpResponseException(HttpStatusCode.Unauthorized); } } return continuation(); } } public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Filters.Add(new CustomAuthorizationFilter());
对于$ expand查询选项中的授权,一个示例。
或者创build每个用户或每个组edm模型。 一个样品。
虽然我认为@SKleanthous提供的解决scheme非常好。 但是, 我们可以做得更好 。 它有一些问题在大多数情况下不会成为问题,我觉得它是足够的问题,我不想让它失去机会。
- 逻辑检查RawExpand属性,它可以有很多东西,它基于嵌套$select和$展开。 这意味着您可以获取信息的唯一合理方法是使用Contains(),这是有缺陷的。
- 被迫使用Contains会导致其他匹配问题,比如说你select一个包含该受限属性的属性作为子串,例如: Orders和OrdersTitle或TotalOrders
- 没有什么可以确定名为订单的属性是您试图限制的“订单types”。 导航属性名称并不是一成不变的,可以在不改变该属性中的魔术string的情况下进行更改。 潜在的维修噩梦。
TL; DR :我们希望保护自己免受特定的实体的侵害,更具体地说,他们的types没有误报。
下面是一个扩展方法,用于从ODataQueryOptions类中获取所有types(技术上为IEdmTypes):
public static class ODataQueryOptionsExtensions { public static List<IEdmType> GetAllExpandedEdmTypes(this ODataQueryOptions self) { //Define a recursive function here. //I chose to do it this way as I didn't want a utility method for this functionality. Break it out at your discretion. Action<SelectExpandClause, List<IEdmType>> fillTypesRecursive = null; fillTypesRecursive = (selectExpandClause, typeList) => { //No clause? Skip. if (selectExpandClause == null) { return; } foreach (var selectedItem in selectExpandClause.SelectedItems) { //We're only looking for the expanded navigation items, as we are restricting authorization based on the entity as a whole, not it's parts. var expandItem = (selectedItem as ExpandedNavigationSelectItem); if (expandItem != null) { //https://msdn.microsoft.com/en-us/library/microsoft.data.odata.query.semanticast.expandednavigationselectitem.pathtonavigationproperty(v=vs.113).aspx //The documentation states: "Gets the Path for this expand level. This path includes zero or more type segments followed by exactly one Navigation Property." //Assuming the documentation is correct, we can assume there will always be one NavigationPropertySegment at the end that we can use. typeList.Add(expandItem.PathToNavigationProperty.OfType<NavigationPropertySegment>().Last().EdmType); //Fill child expansions. If it's null, it will be skipped. fillTypesRecursive(expandItem.SelectAndExpand, typeList); } } }; //Fill a list and send it out. List<IEdmType> types = new List<IEdmType>(); fillTypesRecursive(self.SelectExpand?.SelectExpandClause, types); return types; } }
太好了,我们可以在一行代码中获得所有扩展属性的列表! 太酷了! 让我们在一个属性中使用它:
public class SecureEnableQueryAttribute : EnableQueryAttribute { public List<Type> RestrictedTypes => new List<Type>() { typeof(MyLib.Entities.Order) }; public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions) { List<IEdmType> expandedTypes = queryOptions.GetAllExpandedEdmTypes(); List<string> expandedTypeNames = new List<string>(); //For single navigation properties expandedTypeNames.AddRange(expandedTypes.OfType<EdmEntityType>().Select(entityType => entityType.FullTypeName())); //For collection navigation properties expandedTypeNames.AddRange(expandedTypes.OfType<EdmCollectionType>().Select(collectionType => collectionType.ElementType.Definition.FullTypeName())); //Simply a blanket "If it exists" statement. Feel free to be as granular as you like with how you restrict the types. bool restrictedTypeExists = RestrictedTypes.Select(rt => rt.FullName).Any(rtName => expandedTypeNames.Contains(rtName)); if (restrictedTypeExists) { throw new InvalidOperationException(); } base.ValidateQuery(request, queryOptions); } }
从我可以告诉,唯一的导航属性是EdmEntityType (单一属性)和EdmCollectionType (集合属性)。 获取集合的types名称有一点不同,因为它会将其称为“Collection(MyLib.MyType)”,而不仅仅是“MyLib.MyType”。 我们并不关心它是否是一个集合,所以我们得到了内部元素的types。
我已经在生产代码中使用了一段时间,取得了巨大的成功。 希望您能find与此解决scheme相同的数量。
您可以以编程方式从EDM中删除某些属性:
var employees = modelBuilder.EntitySet<Employee>("Employees"); employees.EntityType.Ignore(emp => emp.Salary);
从http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-security-guidance
把这个移动到你的数据库是否可行? 假设您使用的是SQL Server,请设置符合每个客户端configuration文件所需configuration文件的用户。 保持简单,一个帐户与客户访问和一个没有。
如果您然后将发出数据请求的用户映射到这些configuration文件之一,并修改您的连接string以包含相关的凭据。 那么如果他们向一个实体提出请求,他们将不会被允许,他们将会得到一个例外。
首先,抱歉,如果这是对这个问题的误解。 即使我提出这个build议,我可以看到许多缺陷是数据库中额外的数据访问控制和维护。
另外,我想知道是否可以在生成实体模型的T4模板中完成某些工作。 在关联定义的地方,可以在那里注入一些权限控制。 再次,这将把控制在一个不同的层面 – 我只是把它放在那里,以防谁知道比我更好的T4s可以看到一个办法,使这项工作。
ValidateQuery覆盖将帮助检测用户何时显式扩展或select可导航属性,但是当用户使用通配符时,它不会帮助您。 例如, /Customers?$expand=*
。 相反,你可能想要做的是改变某些用户的模型。 这可以使用EnableQueryAttribute的GetModel覆盖来完成。
例如,首先创build一个方法来生成你的OData模型
public IEdmModel GetModel(bool includeCustomerOrders) { ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); var customerType = builder.EntitySet<Customer>("Customers").EntityType; if (!includeCustomerOrders) { customerType.Ignore(c => c.Orders); } builder.EntitySet<Order>("Orders"); builder.EntitySet<OrderDetail>("OrderDetails"); return build.GetModel(); }
…然后在inheritance自EnableQueryAttribute的类中,覆盖GetModel:
public class SecureAccessAttribute : EnableQueryAttribute { public override IEdmModel GetModel(Type elementClrType, HttpRequestMessage request, HttpActionDescriptor actionDescriptor) { bool includeOrders = /* Check if user can access orders */; return GetModel(includeOrders); } }
请注意,这将在多个调用中创build一堆相同的模型。 考虑caching您的IEdmModel的各种版本,以提高每个呼叫的性能。
您可以将自己的Queryable属性放在Customers.Get()上,或者使用任何一种方法来访问Customers实体(直接或通过导航属性)。 在实现你的属性时,你可以重写ValidateQuery方法来检查访问权限,如下所示:
public class MyQueryableAttribute : QueryableAttribute { public override void ValidateQuery(HttpRequestMessage request, ODataQueryOptions queryOptions) { if (!DoesCurrentUserHaveAccessToCustomers) { throw new ODataException("User cannot access Customer data"); } base.ValidateQuery(request, queryOptions); } }
我不知道你的控制器为什么不被导航属性调用。 它应该是…