如何在呈现 sql 语句之前通过 IQueryable<TEntity> 将授权逻辑注入 EFCore?

How to inject authorization logic through IQueryable<TEntity> into EFCore before a sql statement is rendered?

我目前正在使用 aspnetcoreefcore.

为自己打造一个通用的 OData API

整个通用部分工作正常。然而,获得授权以我需要的方式工作现在是棘手的部分。

从授权的角度来看,我正在寻找的是:

  1. Check if authenticated user has access to a type: very easy to accomplish
  2. property level authorization: aka strip $select=columnA, columnB values from the result
  3. Identity level authorization: aka check if user has access to article with id 1, etc

我的目标是最大性能。

(1) 基本上几乎没有任何努力在针对缓存验证的属性上使用自定义授权。

(2)有没有我可以滥用的钩子来转换什么ODatareturns?本质上,如果用户无权查看某种类型的特定列的值,我想从结果中省略这些列,即使用户选择了它们。

(3) Id 级别授权是一个非常棘手的授权。 我怎样才能在 EFCore 将它变成 SQL 语句之前将自己连接到 IQueryable<TEntity> 状态? 这就是我想要的地方注入我的授权逻辑,以便执行提供最大性能的语句,同时维持功能性分页机制。

我对 (3) 的第一个想法是在 aspnetcore 级别使用结果过滤器,但这可能会使用户处于其分页控件显示 "show max 30 items, page 1/39" 并且他可能会查看页面的状态5 由于他的授权和响应过滤而只有 1 个结果,而不是仅仅获得与他的授权匹配的数据集。

代码可以从https://github.com/taori/Sandbox.git

中提取

欢迎任何关于 (2) 和 (3) 的 pointers/help - 也许有人在这方面有经验并愿意分享。

在过去的几天里,我还没有真正找到任何其他人的授权方法,这些方法需要深入到 EFCore 查询级别。

由于依赖注入,解决方案相当简单。

我转了这个:

[EnableQuery]
[GenericODataControllerNamingConvention]
[Produces("application/json")]
[Route("api/odata/[controller]")]
public class GenericODataController<TEntity> : ODataController
    where TEntity : class
{
    public AdventureWorksContext Context { get; }

    public GenericODataController(AdventureWorksContext context)
    {
        Context = context;
    }

    public IQueryable<TEntity> All() => Context.Set<TEntity>().AsQueryable();

    [Route("{id}")]
    public Task<TEntity> Id(int id) => Context.Set<TEntity>().FindAsync(id);
}

进入这个:

public interface IAuthorization<TEntity>
{
    IQueryable<TEntity> BatchApply(IQueryable<TEntity> source);
    bool Apply(TEntity source);
}

[EnableQuery]
[GenericODataControllerNamingConvention]
[Produces("application/json")]
[Route("api/odata/[controller]")]
public class GenericODataController<TEntity> : ODataController
    where TEntity : class
{
    public AdventureWorksContext Context { get; }

    public ImmutableArray<IAuthorization<TEntity>> Authorizations { get; }

    public GenericODataController(AdventureWorksContext context, IEnumerable<IAuthorization<TEntity>> authorizations)
    {
        Context = context;
        Authorizations = authorizations.ToImmutableArray();
    }

    private TEntity ApplyAuthorization(TEntity source)
    {
        return (Authorizations.Length == 0 || Authorizations.All(d => d.Apply(source))) ? source : default;
    }

    private IQueryable<TEntity> ApplyAuthorizations(IQueryable<TEntity> source)
    {
        var latest = source;
        foreach (var authorization in Authorizations)
        {
            latest = authorization.BatchApply(latest);
        }
        return latest;
    }

    public IQueryable<TEntity> All() => ApplyAuthorizations(Context.Set<TEntity>().AsQueryable());

    [Route("{id}")]
    public async Task<TEntity> Id(int id) => ApplyAuthorization(await Context.Set<TEntity>().FindAsync(id).ConfigureAwait(false));

由于这里使用了依赖注入,我可以注入相关的身份验证参数(例如登录用户)并在 IAuthorization 本身的实现中实现细节。