每个实体的Web API OData安全性

时间:2014-07-27 14:11:25

标签: c# odata asp.net-web-api-odata

背景
我有一个非常大的OData模型,目前正在使用WCF数据服务(OData)来公开它。但是,Microsoft已经声明WCF数据服务是dead,并且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中的特定实体上?(无需枚举所有可能以某种方式扩展到该实体的关联?)

7 个答案:

答案 0 :(得分:42)

  

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

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

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

  1. 使用Web API 2创建新的ASP.Net项目
  2. 创建实体框架数据上下文。
  3. 添加新的&#34; Web API 2 OData控制器...&#34;控制器。
  4. 在WebApiConfigRegister(...)方法中添加以下内容:
  5. 代码:

    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实现如下:
    2. 代码:

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

      请注意,我允许访问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));
          }
      }
      
      1. 在要保护的所有操作中应用该属性。它与EnableQueryAttribute完全一样。可在此处找到完整示例(包括Nuget软件包结束所有内容,使其下载50Mb):http://1drv.ms/1zRmmVj
      2. 我只想对其他一些解决方案发表评论:

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

答案 1 :(得分:17)

当我问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模型。样本。

答案 2 :(得分:4)

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

  1. 逻辑检查RawExpand属性,该属性基于嵌套的$ choices和$ expands可以包含很多东西。这意味着您可以获取信息的唯一合理方法是使用Contains(),这是有缺陷的。
  2. 强制使用Contains会导致其他匹配问题,比如你选择一个包含该受限属性作为子字符串的属性,Ex: Orders 和' OrdersTitle '或者' TotalOrders
  3. 没有什么可以保证名为Orders的属性是您试图限制的“OrderType”。导航属性名称不是一成不变的,并且可以在没有在此属性中更改魔术字符串的情况下进行更改。潜在的维护噩梦。
  4. 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”。我们并不关心它是否是一个集合,所以我们得到内部元素的类型。

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

答案 3 :(得分: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

答案 4 :(得分:0)

将它移到您的数据库是否可行?假设您正在使用SQL Server,请设置与每个客户端配置文件所需的配置文件匹配的用户。保持简单,一个客户访问帐户,一个帐户没有。

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

首先,对不起,如果这是对问题的误解。即使我建议它,我可以看到一些陷阱,最直接的是数据库中的额外数据访问控制和维护。

另外,我想知道是否可以在生成实体模型的T4模板中完成某些事情。在定义关联的地方,可能会在那里注入一些权限控制。再一次,这会把控件放在一个不同的层 - 我只是把它放在那里以防有人知道T4比我更好能看到一种方法来使这项工作。

答案 5 :(得分: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,以提高每次调用的性能。

答案 6 :(得分:-2)

您可以将自己的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);
    }
}

我不知道为什么你的控制器没有被导航属性调用。它应该是......