Web 中的 QueryInterceptor 类似的东西 API

QueryInterceptor similar thing in Web API

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

我在我的 Machine entity 上使用了 QueryInterceptor,它检查当前用户是否有权访问所需数据和 returns 所有数据或允许他们访问的过滤集看。

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

我发现很难在 Web 中实现相同的功能 API。我正在使用 odata v4,OWIN 托管网站 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;
}

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

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

要实现的功能是这样的:

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

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

        Console.WriteLine("End Request");
    }

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

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

While using middleware is a good way to intercept incoming requests for Authentication and Access control, it is not a great way to implement row level security or to manipulate the query used in your controller.

Why? To manipulate the query for the controller, before the request is passed to the controller your middleware code will need to know so much about the controller and the data context that a lot of code will be duplicated.

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

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

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

    /// <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 方法以在机器数据的特定上下文中评估您的安全规则,这将导致您的端点发生以下更改。

Note that I have also included additional endpoints to show how with this pattern ALL endpoints in your controller should use GetQuery() to ensure they have the correct security rules applied. The implication of this pattern though is that A single item Get will return not found instead of access denied if the item is not found because it is out of scope for that user. I prefer this limitation because my user should not have any knowledge that the other data that they are not allowed to access exists.

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