每个实体的Web API OData安全性

本文关键字:API OData 安全性 Web 实体 | 更新日期: 2023-09-27 18:14:59

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

因此,我正在研究让Web API OData与WCF数据服务一样工作的方法。

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

Customers实体有许多可以联系到它的关联。如果计算2个以上级别的关联,则可以通过数百种方式(通过关联(联系到Customers。例如CCD_ 1。由于客户是我的系统的核心,您可以从大多数任何实体开始,并最终关联到客户列表。

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

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

当我看到Web API OData时,我没有看到这样的东西。此外,我非常担心,因为当关联被遵循时,我正在制作的控制器似乎不会被调用。(这意味着我不能在CustomersController中设置安全性。(

我担心我将不得不尝试以某种方式列举协会可以找到客户并为每个客户提供安全保障的所有方式。

问题:
是否有方法对Web API OData中的特定实体设置安全性(无需枚举所有可能以某种方式扩展到该实体的关联?(

每个实体的Web API OData安全性

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

您需要做的是为OData 4创建一个从EnableQueryAttribute继承的新Attribute(或QuerableAttribute,取决于您正在使用的Web API ''OData的版本(,并覆盖ValidateQuery(其方法与从QuerableProperty继承时的方法相同(,以检查是否存在合适的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是我的实体框架实体。配置。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的解决方案并不简单,因为它是相反的,但在其他方面非常接近!事实是,构建器将在实体框架中查看以扩展属性,并且根本不会触及客户控制器!我甚至没有,如果删除安全属性,如果在查询中添加expand命令,它仍然可以检索订单
  2. 设置模型生成器将禁止全局和所有人访问您删除的实体,因此这不是一个好的解决方案
  3. 冯neneneba赵的解决方案可以工作,但你必须手动删除你想在每个查询中安全的项目,无论在哪里,这不是一个好的解决方案

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

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

  1. 该逻辑检查RawExpand属性,该属性中可能有许多基于嵌套的$selects和$expansion的内容。这意味着,获取信息的唯一合理方法是使用Contains((,这是有缺陷的
  2. 被迫使用Contains会导致其他匹配问题,比如您$选择一个包含该受限属性的属性作为子字符串,例如: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.MeType"。我们并不真正关心它是否是集合,所以我们获取内部元素的类型。

我已经在生产代码中使用了一段时间,现在取得了巨大的成功。希望你能用这个解决方案找到同样的数量。

您可以通过编程从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,请为每个客户端配置文件设置与您需要的配置文件相匹配的用户。保持简单,一个帐户有客户访问权限,一个没有。

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

首先,很抱歉,如果这是对问题的误解。尽管我建议这样做,但我可以看到许多陷阱,最直接的是数据库中额外的数据访问控制和维护。

此外,我想知道是否可以在生成实体模型的T4模板中做一些事情。在定义了关联的地方,可能会在那里注入一些权限控制。同样,这将把控制放在另一层——我只是把它放在那里,以防比我更了解T4的人能找到一种方法来实现这一点。

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

例如,首先创建一个方法来生成OData Model

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,以提高每次调用的性能。

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

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