Fine-Grained Auth
Mutations
Authorize writes while SqlOS syncs entity-backed resources.
For create, update, and delete operations, check authorization first with CheckAccessAsync, then mutate your EF entity. If the entity implements ISqlOSResourceEntity and the DbContext derives from SqlOSDbContext<TContext>, SqlOS keeps the backing FGA resource row synchronized when EF saves.
FGA authorizes a subject but does not authenticate the caller. Protect the API route group for its exact audience, and derive the subject from the SqlOS-validated token:
using SqlOS.AuthServer.Extensions;
using SqlOS.Extensions;
const string apiAudience = "https://api.acme.test";
var api = app.MapGroup("/api")
.RequireSqlOSAccessToken(apiAudience);All endpoints below are mapped on that protected api group. Do not accept subjectId from a request body, route value, or unvalidated JWT claim.
ResourceId before save.SqlOSFgaResource row in the same EF save.api.MapPost("/workspaces", async (
CreateWorkspaceRequest request,
AppDbContext db,
ISqlOSFgaAuthService authService,
HttpContext http,
CancellationToken ct) =>
{
var subjectId = http.GetSqlOSValidatedToken()?.UserId;
if (string.IsNullOrWhiteSpace(subjectId))
return Results.Unauthorized();
var organizationResourceId = $"org::{request.OrganizationId}";
var access = await authService.CheckAccessAsync(
subjectId,
"WORKSPACE_MANAGE",
organizationResourceId);
if (!access.Allowed)
return Results.Json(new { error = "Permission denied" }, statusCode: 403);
var workspaceId = Guid.NewGuid();
var workspace = new Workspace
{
Id = workspaceId,
ResourceId = $"workspace::{workspaceId:D}",
OrganizationId = request.OrganizationId,
Name = request.Name.Trim()
};
db.Workspaces.Add(workspace);
await db.SaveChangesAsync(ct);
return Results.Created($"/api/workspaces/{workspace.Id}", workspace);
});api.MapPut("/workspaces/{id}", async (
Guid id,
UpdateWorkspaceRequest request,
AppDbContext db,
ISqlOSFgaAuthService authService,
HttpContext http,
CancellationToken ct) =>
{
var subjectId = http.GetSqlOSValidatedToken()?.UserId;
if (string.IsNullOrWhiteSpace(subjectId))
return Results.Unauthorized();
var workspace = await db.Workspaces.FindAsync([id], ct);
if (workspace == null) return Results.NotFound();
var access = await authService.CheckAccessAsync(
subjectId,
"WORKSPACE_MANAGE",
workspace.ResourceId);
if (!access.Allowed)
return Results.Json(new { error = "Permission denied" }, statusCode: 403);
workspace.Name = request.Name.Trim();
await db.SaveChangesAsync(ct);
return Results.Ok(workspace);
});Because ResourceName => Name, the backing FGA resource display name updates with the entity.
api.MapDelete("/workspaces/{id}", async (
Guid id,
AppDbContext db,
ISqlOSFgaAuthService authService,
HttpContext http,
CancellationToken ct) =>
{
var subjectId = http.GetSqlOSValidatedToken()?.UserId;
if (string.IsNullOrWhiteSpace(subjectId))
return Results.Unauthorized();
var workspace = await db.Workspaces.FindAsync([id], ct);
if (workspace == null) return Results.NotFound();
var access = await authService.CheckAccessAsync(
subjectId,
"WORKSPACE_MANAGE",
workspace.ResourceId);
if (!access.Allowed)
return Results.Json(new { error = "Permission denied" }, statusCode: 403);
db.Workspaces.Remove(workspace);
await db.SaveChangesAsync(ct);
return Results.NoContent();
});Deletes remove the backing resource and direct grants. If child resources still reference the resource, SqlOS throws a clear exception so the application can delete or reparent children first.