Fine-Grained Auth
Detail Endpoints
Fetch and authorize a single entity in one call.
AuthorizedDetailAsync fetches a single entity, checks authorization, and returns the appropriate HTTP result. It handles 404, 403, and 200 in one call.
This helper is for read-side authorization. Lifecycle-managed domain rows normally implement ISqlOSResourceEntity; AuthorizedDetailAsync only needs the inherited IHasResourceId contract to check access for a fetched row.
It does not authenticate the caller. Validate a bearer token for this API's exact audience before running the FGA check, and derive the subject from that validated token rather than from client input or an unvalidated JWT.
using SqlOS.AuthServer.Extensions;
using SqlOS.Extensions;
const string apiAudience = "https://api.acme.test";
var api = app.MapGroup("/api")
.RequireSqlOSAccessToken(apiAudience);
api.MapGet("/chains/{id}", async (
string id,
ExampleAppDbContext context,
ISqlOSFgaAuthService authService,
HttpContext http) =>
{
var subjectId = http.GetSqlOSValidatedToken()?.UserId;
if (string.IsNullOrWhiteSpace(subjectId))
return Results.Unauthorized();
return await authService.AuthorizedDetailAsync(
context.Chains.Include(c => c.Locations),
c => c.Id == id,
subjectId, "CHAIN_VIEW",
chain => new ChainDetailDto
{
Id = chain.Id,
ResourceId = chain.ResourceId,
Name = chain.Name,
Description = chain.Description,
LocationCount = chain.Locations.Count,
CreatedAt = chain.CreatedAt
});
});| Condition | HTTP result |
|---|---|
| Entity not found | 404 Not Found |
| Subject lacks permission | 403 { error: "Permission denied" } |
| Access granted | 200 with mapped DTO |
Task<IResult> AuthorizedDetailAsync<TEntity, TDto>(
IQueryable<TEntity> query,
Expression<Func<TEntity, bool>> predicate,
string subjectId,
string permissionKey,
Func<TEntity, TDto> selector) where TEntity : class, IHasResourceId;Use AuthorizedDetailAsync for any GET /resource/{id} endpoint. It replaces the common pattern of:
var entity = await query.FirstOrDefaultAsync(e => e.Id == id);
if (entity == null) return Results.NotFound();
var access = await authService.CheckAccessAsync(subjectId, "CHAIN_VIEW", entity.ResourceId);
if (!access.Allowed) return Results.Json(new { error = "Permission denied" }, statusCode: 403);
return Results.Ok(MapToDto(entity));