每个实体的Web API OData安全性

Vac*_*ano 46 c# odata asp.net-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();
}
Run Code Online (Sandbox Code Playgroud)

当我看到Web API OData时,我没有看到这样的东西.另外,我非常担心,因为我正在制作的控制器在跟随关联时似乎没有被调用.(意思是我不能把安全放在CustomersController.)

我担心我将不得不尝试以某种方式列举协会如何获得客户并为每个客户提供安全性的所有方式.

问题:
有没有办法将安全性放在Web API OData中的特定实体上? (无需枚举所有可能以某种方式扩展到该实体的关联?)

SKl*_*ous 44

更新:此时我建议您遵循由vaccano发布的解决方案,该解决方案基于OData团队的输入.

您需要做的是创建一个继承自EnableQueryAttribute for OData 4的新属性(或QuerableAttribute,具体取决于您正在与之交谈的Web API\OData的版本)并覆盖ValidateQuery(与继承自QuerableAttribute时的方法相同)检查是否存在合适的SelectExpand属性.

要设置新的新项目以进行测试,请执行以下操作:

  1. 使用Web API 2创建一个新的ASP.Net项目
  2. 创建实体框架数据上下文.
  3. 添加一个新的"Web API 2 OData Controller ..."控制器.
  4. 在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());
Run Code Online (Sandbox Code Playgroud)

在上文中,Customer,Order和OrderDetail是我的实体框架实体.config.AddODataQueryFilter(新的SecureAccessAttribute())注册我的SecureAccessAttribute以供使用.

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

请注意,我允许访问Customers控制器,但我限制对订单的访问.我实施的唯一控制器如下:

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));
    }
}
Run Code Online (Sandbox Code Playgroud)
  1. 在要保护的所有操作中应用该属性.它与EnableQueryAttribute完全一样.一个完整的示例(包括Nuget包结束所有内容,使其下载50Mb)可以在这里找到:http://1drv.ms/1zRmmVj

我只想对其他一些解决方案发表评论:

  1. Leyenda的解决方案不起作用仅仅因为它是另一种方式,但在其他方面非常接近!事实是,构建器将查看实体框架以扩展属性,并且根本不会访问Customers控制器!我甚至没有一个,如果你删除了安全属性,如果你将expand命令添加到你的查询,它仍然会检索订单.
  2. 设置模型构建器将禁止访问您在全局和所有人中删除的实体,因此这不是一个好的解决方案.
  3. Feng Zhao的解决方案可以工作,但你必须手动删除你想在每个查询中保护的项目,无处不在,这不是一个好的解决方案.


Vac*_*ano 18

当我问Web API OData团队时,我得到了这个答案.它似乎与我接受的答案非常相似,但它使用了IAuthorizationFilter.

为了完整性,我想我会在这里发布:


对于实体集或导航属性出现在路径中,我们可以定义消息处理程序或授权过滤器,并在该检查中检查用户请求的目标实体集.例如,一些代码片段:

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

对于查询选项中的$ expand授权,一个示例.

或者为每个用户或每组edm模型创建.一个样品.

  • Vaccano,这似乎是一个很好的方式.我将进一步调查并更新我的答案,如果它确实是一个例子.不幸的是,由于假期,这将在一周左右发生:) (2认同)

Zac*_*Dow 5

虽然我认为@SKleanthous 提供的解决方案非常好。但是,我们可以做得更好。它有一些在大多数情况下不会成为问题的问题,我觉得它们已经足够了,我不想让它碰运气。

  1. 逻辑检查 RawExpand 属性,它可以包含很多基于嵌套的 $selects 和 $expands 的东西。这意味着您可以获取信息的唯一合理方法是使用 Contains(),这是有缺陷的。
  2. 被迫使用 Contains 会导致其他匹配问题,例如您 $select 包含该受限属性作为子字符串的属性,例如:Orders和 ' OrdersTitle ' 或 ' TotalOrders '
  3. 没有什么可以保证名为 Orders 的属性属于您试图限制的“OrderType”。导航属性名称不是一成不变的,并且可以在不更改此属性中的魔术字符串的情况下进行更改。潜在的维护噩梦。

TL;DR:我们希望保护自己免受特定实体的侵害,但更具体地说,它们的类型没有误报。

这是从 ODataQueryOptions 类中获取所有类型(技术上为 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

太好了,我们可以在一行代码中获得所有扩展属性的列表!这很酷!让我们在属性中使用它:

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

}
Run Code Online (Sandbox Code Playgroud)

据我所知,唯一的导航属性是EdmEntityType(单一属性)和EdmCollectionType(集合属性)。获取集合的类型名称有点不同,因为它会将其称为“Collection(MyLib.MyType)”而不仅仅是“MyLib.MyType”。我们并不真正关心它是否是一个集合,所以我们得到了内部元素的类型。

我已经在生产代码中使用它一段时间了,并取得了巨大的成功。希望您会在此解决方案中找到相同的数量。