49

背景:
我有一个非常大的 OData 模型,目前正在使用 WCF 数据服务 (OData) 来公开它。然而,微软已经声明 WCF 数据服务已经死了,而 Web API OData 是他们将要走的路。

所以我正在研究如何让 Web API OData 和 WCF 数据服务一样工作。

问题设置:
模型的某些部分不需要固定,但有些部分需要固定。例如,客户列表需要安全性来限制谁可以阅读它,但我还有其他列表,例如产品列表,任何人都可以查看。

客户实体有许多可以访问它的关联。如果您计算 2 级以上的关联,则可以通过数百种方式联系客户(通过关联)。例如Prodcuts.First().Orders.First().Customer. 由于客户是我系统的核心,因此您可以从大多数实体开始,并最终将您的方式与客户列表相关联。

WCF 数据服务有一种方法可以让我通过如下方法对特定实体设置安全性:

[QueryInterceptor("Customers")]
public Expression<Func<Customer, bool>> CheckCustomerAccess()
{
     return DoesCurrentUserHaveAccessToCustomers();
}

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

我担心我将不得不尝试以某种方式列举关联可以如何接触客户并为每一个提供安全性的所有方式。

问题:
有没有办法在 Web API OData 中为特定实体设置安全性? (不必列举所有可能以某种方式扩展到该实体的关联?)

4

7 回答 7

47

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

您需要做的是为 OData 4 创建一个继承自 EnableQueryAttribute 的新属性(或 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());

在上面,Customer、Order 和 OrderDetail 是我的实体框架实体。config.AddODataQueryFilter(new 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);
    }
}

请注意,我允许访问客户控制器,但我限制访问订单。我实现的唯一控制器是以下一个:

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));
    }
}
  1. 在要保护的所有操作中应用该属性。它与 EnableQueryAttribute 完全相同。可以在此处找到完整的示例(包括 Nuget 包结束所有内容,下载大小为 50Mb):http: //1drv.ms/1zRmmVj

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

  1. Leyenda 的解决方案不能仅仅因为它是相反的方式而起作用,但除此之外非常接近!事实是,构建器将在实体框架中查看以扩展属性,并且根本不会触及客户控制器!我什至没有,如果您删除安全属性,如果您将扩展命令添加到查询中,它仍然可以正常检索订单。
  2. 设置模型构建器将禁止访问您全局和所有人删除的实体,因此这不是一个好的解决方案。
  3. Feng Zhao 的解决方案可以工作,但是您必须在每个查询中手动删除您想要保护的项目,这不是一个好的解决方案。
于 2014-08-03T15:34:21.377 回答
19

当我询问 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());

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

或按用户或按组创建 edm 模型。一个样品。

于 2014-08-07T17:51:44.970 回答
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;
    }
}

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

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(集合属性)。获取集合的类型名称有点不同,因为它将调用它“Collection(MyLib.MyType)”而不是“MyLib.MyType”。我们并不关心它是否是一个集合,所以我们得到了内部元素的类型。

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

于 2015-11-09T22:02:55.427 回答
1

您可以通过编程方式从 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

于 2014-08-01T18:36:20.963 回答
0

将其移至您的数据库是否可行?假设您使用的是 SQL 服务器,请设置与每个客户端配置文件所需的配置文件相匹配的用户。保持简单,一个帐户可以访问客户,一个帐户没有。

如果您随后将发出数据请求的用户映射到这些配置文件之一,并修改您的连接字符串以包含相关凭据。然后,如果他们向不允许的实体提出请求,他们将获得例外。

首先,对不起,如果这是对问题的误解。尽管我建议这样做,但我可以看到一些最直接的缺陷是您的数据库中的额外数据访问控制和维护。

另外,我想知道是否可以在生成实体模型的 T4 模板中完成某些操作。在定义关联的地方,可能会在那里注入一些权限控制。同样,这会将控件置于不同的层 - 我只是将它放在那里,以防比我更了解 T4 的人可以找到一种方法来完成这项工作。

于 2014-08-02T20:40:28.693 回答
0

ValidateQuery 覆盖将有助于检测用户何时显式展开或选择可导航属性,但是当用户使用通配符时它不会帮助您。例如,/Customers?$expand=*。相反,您可能想要做的是更改某些用户的模型。这可以使用 EnableQueryAttribute 的 GetModel 覆盖来完成。

例如,首先创建一个方法来生成您的 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();
}

...然后在继承自 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);
    }
}

请注意,这将在多个调用中创建一堆相同的模型。考虑缓存各种版本的 IEdmModel 以提高每次调用的性能。

于 2016-12-29T21:56:01.570 回答
-2

您可以将自己的 Queryable 属性放在 Customers.Get() 或用于访问客户实体的任何方法(直接或通过导航属性)。在您的属性的实现中,您可以重写 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);
    }
}

我不知道为什么不在导航属性上调用您的控制器。它应该是...

于 2014-08-02T23:49:19.427 回答