Fine-Grained Auth
List Filtering
Filter EF Core queries by authorization at query time.
GetAuthorizationFilterAsync returns a filter for Where(...). Lists only rows the subject may see. Use this on list endpoints.
This is a read-side helper. Lifecycle-managed domain rows normally implement ISqlOSResourceEntity; the filter only needs the inherited ResourceId contract to join application rows to accessible FGA resources.
The filter authorizes a subject ID; it does not authenticate the request. Validate a bearer token for the API's exact audience before the endpoint runs, then use the validated token's user ID as the FGA subject.
var filter = await authService
.GetAuthorizationFilterAsync<Chain>(subjectId, "CHAIN_VIEW");
var chains = await dbContext.Chains
.Where(filter)
.OrderBy(c => c.Name)
.ToListAsync();The subject only sees chains they have access to. The filter translates to a SQL query -- no in-memory filtering.
Use PagedSpec for built-in pagination, sorting, search, and authorization:
var spec = PagedSpec.For<Chain>(c => c.Id)
.RequirePermission("CHAIN_VIEW")
.SortByString("name", c => c.Name, isDefault: true)
.Search(searchQuery, c => c.Name, c => c.Description)
.Build(pageSize: 20, cursor, sortBy, sortDir);
var result = await executor.ExecuteAsync(
dbContext.Chains, spec, subjectId,
chain => new ChainDto
{
Id = chain.Id,
Name = chain.Name,
Description = chain.Description,
CreatedAt = chain.CreatedAt
});
// result.Data → authorized, searched, sorted, paged
// result.NextCursor → cursor for next page
// result.HasNextPage → whether more results existFrom the retail example app:
using SqlOS.AuthServer.Extensions;
using SqlOS.Extensions;
const string apiAudience = "https://api.acme.test";
var api = app.MapGroup("/api")
.RequireSqlOSAccessToken(apiAudience);
api.MapGet("/chains", async (
ExampleAppDbContext context,
ISqlOSFgaAuthService authService,
ISpecificationExecutor executor,
HttpContext http,
string? search, int? pageSize, string? cursor,
string? sortBy, string? sortDir) =>
{
var subjectId = http.GetSqlOSValidatedToken()?.UserId;
if (string.IsNullOrWhiteSpace(subjectId))
return Results.Unauthorized();
var spec = PagedSpec.For<Chain>(c => c.Id)
.RequirePermission(RetailPermissionKeys.ChainView)
.SortByString("name", c => c.Name, isDefault: true)
.Search(search, c => c.Name, c => c.Description)
.Build(pageSize ?? 20, cursor, sortBy, sortDir);
return await executor.ExecuteAsync(
context.Chains.Include(c => c.Locations),
spec, subjectId,
chain => new ChainListDto { /* ... */ });
});For single-resource access, use AuthorizedDetailAsync. For mutations, use CheckAccessAsync.