QueryInterceptor在Web API中类似的东西

时间:2017-03-13 10:10:26

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

我实际上是将以前的WCF服务的某些部分迁移到Web API。

我在我的QueryInterceptor上使用Machine entity来检查当前用户是否可以访问所需数据并返回允许他们看到的所有数据或过滤集。

[QueryInterceptor("Machines")]
public Expression<Func<Machine, bool>> FilterMachines()
{
     return CheckMachineAccess<Machine>(m => m.MachineRole==xyz && m.userHasPermission);
}

我发现很难在Web API中实现相同的功能。我使用odata v4,OWIN托管了Web API。

有人对此有任何建议吗?在此先感谢:)

修改 我遵循这种方法。不知道这是否是正确的方法。

[HttpGet]
[ODataRoute("Machines")]
[EnableQuery]
public IQueryable<Machine> FilterMachines(ODataQueryOptions opts)
{
     var expression = CheckMachineAccess<Machine>(m => m.MachineRole==xyz && m.userHasPermission);

     var result = db.Machines.Where(expression);

     return (IQueryable<Machine>)result;
}

2 个答案:

答案 0 :(得分:1)

OP你是在正确的轨道,如果这对你有用,那么我完全支持它!

我先直接解决你问题的标题。

  

虽然使用中间件是拦截传入的身份验证和访问控制请求的好方法,但它不是实现行级安全性或操纵控制器中使用的查询的好方法。

     

为什么呢?为了操纵控制器的查询,在将请求传递给控制器​​之前,中间件代码需要了解控制器和数据上下文,以便复制大量代码。

在OData服务中,许多QueryInterceptor实现的一个很好的替代是从EnableQuery属性继承。

[AttributeUsage(validOn: AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class EnableQueryAttribute : System.Web.OData.EnableQueryAttribute
{
    public EnableQueryAttribute()
    {
        // TODO: Reset default values
    }

    /// <summary>
    /// Intercept before the query, here we can safely manipulate the URL before the WebAPI request has been processed so before the OData context has been resolved.
    /// </summary>
    /// <remarks>Simple implementation of common url replacement tasks in OData</remarks>
    /// <param name="actionContext"></param>
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var tokens = HttpUtility.ParseQueryString(actionContext.Request.RequestUri.AbsoluteUri);
        // If the caller requested oDataV2 $inlinecount then remove it!
        if (tokens.AllKeys.Contains("$inlinecount"))
        {
            // CS: we don't care what value they requested, OData v4 will only return the allPages count
            tokens["$count"] = "true";
            tokens.Remove("$inlinecount");
        }
        // if caller forgot to ask for count and we are top'ing but paging hasn't been configured lets add the overall count for good measure
        else if (String.IsNullOrEmpty(tokens["$count"])
            && !String.IsNullOrEmpty(tokens["$top"])
            && this.PageSize <= 0
        )
        {
            // we want to add $count if it is not there
            tokens["$count"] = "true";
        }

        var modifiedUrl = ParseUri(tokens);

        // if we modified the url, reset it. Leaving this in a logic block to make an obvious point to extend the process, say to perform other clean up when we know we have modified the url
        if (modifiedUrl != actionContext.Request.RequestUri.AbsoluteUri)
            actionContext.Request.RequestUri = new Uri(modifiedUrl);

        base.OnActionExecuting(actionContext);
    }

    /// <summary>
    /// Simple validator that can fix common issues when converting NameValueCollection back to Uri when the collection has been modified.
    /// </summary>
    /// <param name="tokens"></param>
    /// <returns></returns>
    private static string ParseUri(System.Collections.Specialized.NameValueCollection tokens)
    {
        var query = tokens.ToHttpQuery().TrimStart('=');
        if (!query.Contains('?')) query = query.Insert(query.IndexOf('&'), "?");
        return query.Replace("?&", "?");
    }

    /// <summary>
    /// Here we can intercept the IQueryable result AFTER the controller has processed the request and created the intial query.
    /// </summary>
    /// <remarks>
    /// So you could append filter conditions to the query, but, like middleware you may need to know a lot about the controller 
    /// or you have to make a lot of assumptions to make effective use of this override. Stick to operations that modify the queryOptions 
    /// or that conditionally modify the properties on this EnableQuery attribute
    /// </remarks>
    /// <param name="queryable">The original queryable instance from the controller</param>
    /// <param name="queryOptions">The System.Web.OData.Query.ODataQueryOptions instance constructed based on the incomming request</param>
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
        // I do not offer common examples of this override, because they would be specific to your business logic, but know that it is an available option
        return base.ApplyQuery(queryable, queryOptions);
    }
}

但是,我们如何解决有效实施行级安全问题的问题? 你已经实现的与我的工作非常相似。你是对的,在你的控制器方法中,你有足够的信息 上下文,以便能够将过滤器应用于您的查询。

我的项目中有类似的想法,并且我的所有控制器都有一个公共基类,它有一个方法,所有继承控制器必须使用它来获取各自实体类型的初始过滤查询: 以下是我的基类方法的简化版本,用于将安全样式规则应用于查询

    /// <summary>
    /// Get the base table query for this entity, with user policy applied
    /// </summary>
    /// <returns>Default IQueryable reference to use in this controller</returns>
    protected Task<IQueryable<TEntity>> GetQuery()
    {
        var dbQuery = this.GetEntityQuery();
        return this.ApplyUserPolicy(dbQuery);
    }

    /// <summary>
    /// Inheriting classes MUST override this method to include standard related tables to the DB query
    /// </summary>
    /// <returns></returns>
    protected abstract DbQuery<TEntity> GetEntityQuery();

    /// <summary>
    /// Apply default user policy to the DBQuery that will be used by actions on this controller.
    /// </summary>
    /// <remarks>
    /// Allow inheriting classes to implement or override the DBQuery before it is parsed to an IQueryable, note that you cannot easily add include statements once it is IQueryable
    /// </remarks>
    /// <param name="dataTable">DbQuery to parse</param>
    /// <param name="tokenParameters">Security and Context Token variables that you can apply if you want to</param>
    /// <returns></returns>
    protected virtual IQueryable<TEntity> ApplyUserPolicy(DbQuery<TEntity> dataTable, System.Collections.Specialized.NameValueCollection tokenParameters)
    {
        // TODO: Implement default user policy filtering - like filter by tenant or customer.

        return dataTable;
    }

现在,在您的控制器中,您将覆盖ApplyUserPolicy方法,以评估计算机数据的特定上下文中的安全规则,这将导致对端点进行以下更改。

  

请注意,我还添加了其他端点,以显示控制器中此模式的所有端点    应该使用GetQuery()来确保它们应用了正确的安全规则。   这种模式的含义是,如果项目不是,则单个项目Get将返回未找到而不是拒绝访问    发现是因为它超出了该用户的范围。我更喜欢这个限制,因为我的用户不应该知道其他数据    他们不被允许访问存在。

    /// <summary>
    /// Check that User has permission to view the rows and the required role level
    /// </summary>
    /// <remarks>This applies to all queries on this controller</remarks>
    /// <param name="dataTable">Base DbQuery to parse</param>
    /// <returns></returns>
    protected override IQueryable<Machine> ApplyUserPolicy(DbQuery<Machine> dataTable)
    {
        // Apply base level policies, we only want to add further filtering conditions, we are not trying to circumvent base level security
        var query = base.ApplyUserPolicy(dataTable, tokenParameters);

        // I am faking your CheckMachineAccess code, as I don't know what your logic is
        var role = GetUserRole();
        query = query.Where(m => m.MachineRole == role);

        // additional rule... prehaps user is associated to a specific plant or site and con only access machines at that plant
        var plant = GetUserPlant();
        if (plant != null) // Maybe plant is optional, so admin users might not return a plant, as they can access all machines
        {
            query = query.Where(m => m.PlantId == plant.PlantId);
        }

        return query;
    }

    [HttpGet]
    [ODataRoute("Machines")]
    [EnableQuery]
    public IQueryable<Machine> FilterMachines(ODataQueryOptions opts)
    {
        // Get the default query with security applied
        var expression = GetQuery();

        // TODO: apply any additional queries specific to this endpoint, if there are any

        return expression;
    }

    [HttpGet]
    [ODataRoute("Machine")]
    [EnableQuery] // so we can still apply $select and $expand
    [HttpGet]
    public SingleResult<Machine> GetMachine([FromODataUri] int key)
    { 
        // Get the default query with security applied
        var query = GetQuery();
        // Now filter for just this item by id
        query = query.Where(m => m.Id == key);

        return SingleResult.Create(query);
    }


    [HttpGet]
    [ODataRoute("MachinesThatNeedService")]
    [EnableQuery]
    internal IQueryable<Machine> GetMachinesServiceDue(ODataQueryOptions opts)
    {
        // Get the default query with security applied
        var query = GetQuery();
        // apply the specific filter for this endpoint
        var lastValidServiceDate = DateTimeOffset.Now.Add(-TimeSpan.FromDays(60));
        query = query.Where(m => m.LastService < lastValidServiceDate);

        return query;
    }

答案 1 :(得分:0)

您可以使用OWIN middelware输入请求的管道。

您将拥有一个带有HTTP请求的功能,您可以决定接受或拒绝该请求。

要实现的功能是这样的:

public async override Task Invoke(IOwinContext context)
    {
        // here do your check!!

        if(isValid)
        {
            await Next.Invoke(context);
        }

        Console.WriteLine("End Request");
    }