如何在呈现 sql 语句之前通过 IQueryable<TEntity> 将授权逻辑注入 EFCore?
How to inject authorization logic through IQueryable<TEntity> into EFCore before a sql statement is rendered?
我目前正在使用 aspnetcore
和 efcore
.
为自己打造一个通用的 OData API
整个通用部分工作正常。然而,获得授权以我需要的方式工作现在是棘手的部分。
从授权的角度来看,我正在寻找的是:
- Check if authenticated user has access to a type: very easy to
accomplish
- property level authorization: aka strip
$select=columnA, columnB
values from the result
- Identity level authorization: aka check if user has access to article with
id
1, etc
我的目标是最大性能。
(1) 基本上几乎没有任何努力在针对缓存验证的属性上使用自定义授权。
(2)有没有我可以滥用的钩子来转换什么OData
returns?本质上,如果用户无权查看某种类型的特定列的值,我想从结果中省略这些列,即使用户选择了它们。
(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 本身的实现中实现细节。
我目前正在使用 aspnetcore
和 efcore
.
OData API
整个通用部分工作正常。然而,获得授权以我需要的方式工作现在是棘手的部分。
从授权的角度来看,我正在寻找的是:
- Check if authenticated user has access to a type: very easy to accomplish
- property level authorization: aka strip
$select=columnA, columnB
values from the result- Identity level authorization: aka check if user has access to article with
id
1, etc
我的目标是最大性能。
(1) 基本上几乎没有任何努力在针对缓存验证的属性上使用自定义授权。
(2)有没有我可以滥用的钩子来转换什么OData
returns?本质上,如果用户无权查看某种类型的特定列的值,我想从结果中省略这些列,即使用户选择了它们。
(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 本身的实现中实现细节。