Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,29 @@ namespace CodeBeam.UltimateAuth.IdentityLab.Authorization;

public sealed class LabAccessContextProvider(IAuthFlow authFlow, IAccessContextFactory contextFactory)
{
// Authenticated management contexts must originate in an HTTP endpoint.
public async Task<AccessContext> CreateAsync(string action, string resource, CancellationToken cancellationToken = default)
{
var validated = await CreateValidatedAsync(action, resource, cancellationToken);
return validated.Context;
}

public async Task<(AuthFlowContext Flow, AccessContext Context)> CreateValidatedAsync(string action, string resource, CancellationToken cancellationToken = default)
{
var flow = await authFlow.BeginAsync(AuthFlowType.ValidateSession, cancellationToken);
return await contextFactory.CreateAsync(flow, action, resource, ct: cancellationToken);
var context = await contextFactory.CreateAsync(flow, action, resource, ct: cancellationToken);
return (flow, context);
}


public async Task<AccessContext> CreateForUserAsync(string action, string resource, UserKey targetUserKey, CancellationToken ct = default)
public Task<AccessContext> CreateForValidatedUserAsync(AuthFlowContext flow, AccessContext actor, string action, string resource, UserKey targetUserKey, CancellationToken ct = default)
{
var flow = await authFlow.BeginAsync(AuthFlowType.ValidateSession, ct);
return await contextFactory.CreateAsync(flow, action, resource, resourceId: targetUserKey.Value, ct: ct);
// Reuse only the flow validated in this request, retaining actor, tenant and device validation.
if (!flow.IsAuthenticated || flow.UserKey is null ||
!actor.IsAuthenticated || actor.IsSystemActor ||
flow.UserKey != actor.ActorUserKey || flow.Tenant != actor.ActorTenant || actor.IsCrossTenant)
throw new UnauthorizedAccessException("Rollback actor mismatch.");

return contextFactory.CreateAsync(flow, action, resource, resourceId: targetUserKey.Value, ct: ct);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@

namespace CodeBeam.UltimateAuth.IdentityLab.Endpoints;

public sealed class LabAdminAuthorizationFilter(LabAccessContextProvider contextProvider, ILabAdminGuard adminGuard) : IEndpointFilter
public sealed class LabAdminAuthorizationFilter(ILabAdminGuard adminGuard) : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext invocation,
EndpointFilterDelegate next)
{
var context = await contextProvider.CreateAsync(
"identitylab.admin",
"identitylab",
invocation.HttpContext.RequestAborted);
var context = LabManagementAuthorizationFilter.Context(invocation.HttpContext);

if (!context.IsAuthenticated || context.ActorUserKey is null)
return Results.Unauthorized();
Expand All @@ -19,7 +16,7 @@ public sealed class LabAdminAuthorizationFilter(LabAccessContextProvider context
context,
invocation.HttpContext.RequestAborted))
{
return Results.Forbid();
return Results.StatusCode(StatusCodes.Status403Forbidden);
}

return await next(invocation);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using CodeBeam.UltimateAuth.Core.Contracts;
using CodeBeam.UltimateAuth.Server.Auth;
using CodeBeam.UltimateAuth.Core.MultiTenancy;
using CodeBeam.UltimateAuth.IdentityLab.Authorization;
using Microsoft.AspNetCore.Antiforgery;

namespace CodeBeam.UltimateAuth.IdentityLab.Endpoints;

// A request scope owns the validated context. No component state or payload supplies the actor.
public sealed class LabManagementAuthorizationFilter(
LabAccessContextProvider contextProvider,
IAntiforgery antiforgery,
ILogger<LabManagementAuthorizationFilter> logger) : IEndpointFilter
{
public static AccessContext Context(HttpContext http) => (AccessContext)http.Items[typeof(AccessContext)]!;

public static AuthFlowContext Flow(HttpContext http) => (AuthFlowContext)http.Items[typeof(AuthFlowContext)]!;

public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext invocation, EndpointFilterDelegate next)
{
var http = invocation.HttpContext;
try
{
if (!HttpMethods.IsGet(http.Request.Method))
{
try { await antiforgery.ValidateRequestAsync(http); }
catch (AntiforgeryValidationException) { return Results.BadRequest(); }
}

var operation = http.GetEndpoint()!.Metadata.GetMetadata<LabOperation>()!;
var (flow, context) = await contextProvider.CreateValidatedAsync(operation.Action, operation.Resource, http.RequestAborted);
if (!context.IsAuthenticated || context.ActorUserKey is null)
return Results.Unauthorized();
if (context.IsSystemActor || context.ActorTenant != TenantKeys.Single || context.IsCrossTenant)
return Results.StatusCode(StatusCodes.Status403Forbidden);

http.Items[typeof(AccessContext)] = context;
http.Items[typeof(AuthFlowContext)] = flow;
return await next(invocation);
}
catch (UnauthorizedAccessException) { return Results.StatusCode(StatusCodes.Status403Forbidden); }
catch (OperationCanceledException) when (http.RequestAborted.IsCancellationRequested) { throw; }
catch (Exception ex)
{
logger.LogError(ex, "Identity Lab management request failed for {Path}", http.Request.Path);
return Results.Problem("Unable to complete the operation.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
@using CodeBeam.UltimateAuth.IdentityLab.Contracts
@using CodeBeam.UltimateAuth.IdentityLab.Services

@inject LabRepairService RepairService
@inject LabAccessContextProvider ContextProvider
@inject LabManagementClient Management
@inject IDialogService DialogService
@inject ISnackbar Snackbar

Expand Down Expand Up @@ -156,23 +155,14 @@
private bool _busy;
private string? _error;

private async Task<CodeBeam.UltimateAuth.Core.Contracts.AccessContext>
GetContextAsync()
{
return await ContextProvider.CreateAsync(
"identitylab.repair.admin",
"identitylab.repair");
}

private async Task LoadAsync()
{
_busy = true;
_error = null;

try
{
var context = await GetContextAsync();
_snapshot = await RepairService.GetSnapshotAsync(context);
_snapshot = await Management.SendAsync<RepairSnapshot>("/api/lab/admin/repair/scan");
}
catch (Exception)
{
Expand All @@ -197,8 +187,7 @@

await ExecuteAsync(async () =>
{
var context = await GetContextAsync();
return await RepairService.RepairOwnerAsync(context, user.UserKey);
return await Management.SendAsync<RepairOperationResult>($"/api/lab/admin/repair/owner/{Uri.EscapeDataString(user.UserKey.Value)}");
});
}

Expand All @@ -215,8 +204,7 @@

await ExecuteAsync(async () =>
{
var context = await GetContextAsync();
return await RepairService.DeleteEmptySandboxAsync(context, sandbox.SandboxId);
return await Management.SendAsync<RepairOperationResult>($"/api/lab/admin/repair/sandbox/{sandbox.SandboxId}/delete");
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@using CodeBeam.UltimateAuth.IdentityLab.Authorization
@using CodeBeam.UltimateAuth.IdentityLab.Services

@inject LabDemoStoreService DemoStoreService
@inject LabManagementClient Management
@inject ISnackbar Snackbar

<PageTitle>Demo Store · Identity Lab</PageTitle>
Expand Down Expand Up @@ -221,9 +221,11 @@
private bool Has(string permission) =>
_permissions.Contains(permission);

protected override async Task OnInitializedAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
await RefreshAccessAsync();
StateHasChanged();
}

private async Task RefreshAccessAsync()
Expand All @@ -234,7 +236,7 @@
try
{
_permissions =
await DemoStoreService.GetPermissionsAsync();
await Management.SendAsync<HashSet<string>>("/api/lab/manage/demo-permissions");
}
catch (Exception)
{
Expand All @@ -259,7 +261,7 @@

try
{
await DemoStoreService.RequireAsync(permission);
await Management.SendAsync<bool>("/api/lab/manage/demo-permissions/check", new { Permission = permission });

operation();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
@using CodeBeam.UltimateAuth.Users.Contracts
@layout DashboardLayout
@inject SandboxUserService SandboxUserService
@inject LabManagementClient Management
@inject IDialogService DialogService
@inject ISnackbar Snackbar

Expand Down Expand Up @@ -407,7 +408,14 @@
if (actorUserKey is null)
return CreateUserDialogResult.Failure("You must be signed in.");

return await SandboxUserService.CreateAsync(actorUserKey.Value, request);
try
{
return await Management.SendAsync<CreateUserDialogResult>("/api/lab/manage/users", request);
}
catch (UnauthorizedAccessException)
{
return CreateUserDialogResult.Failure("You are not allowed to manage this sandbox.");
}
}

private async Task DeleteUserAsync(SandboxUserItem user)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
@attribute [UAuthAuthorize(Roles = "Ladmin,Owner,User")]

@inject LabDashboardService DashboardService
@inject LabOwnerRepairService OwnerRepairService
@inject LabAccessContextProvider ContextProvider
@inject LabManagementClient Management
@inject ISnackbar Snackbar

<PageTitle>Overview · Identity Lab</PageTitle>
Expand Down Expand Up @@ -482,11 +481,7 @@

if (_dashboard is null)
{
var context = await ContextProvider.CreateAsync(
"identitylab.owner.repair.status",
"identitylab.sandbox");

_repairStatus = await OwnerRepairService.GetStatusAsync(context);
_loadRepairStatus = true;
}

_loadedUserKey = userKey;
Expand All @@ -506,6 +501,23 @@
}
}

private bool _loadRepairStatus;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_loadRepairStatus) return;
_loadRepairStatus = false;
try
{
_repairStatus = await Management.SendAsync<OwnerRepairStatus>("/api/lab/manage/owner-repair/status");
}
catch (Exception)
{
Snackbar.Add("Unable to load sandbox recovery status.", Severity.Error);
}
StateHasChanged();
}

private async Task RepairSandboxAsync()
{
if (_repairing)
Expand All @@ -515,11 +527,7 @@

try
{
var context = await ContextProvider.CreateAsync(
"identitylab.owner.repair",
"identitylab.sandbox");

var result = await OwnerRepairService.RepairAsync(context);
var result = await Management.SendAsync<OwnerRepairResult>("/api/lab/manage/owner-repair");

Snackbar.Add(
result.Message,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@using CodeBeam.UltimateAuth.IdentityLab.Authorization
@using CodeBeam.UltimateAuth.IdentityLab.Services

@inject LabDemoRoleService DemoRoleService
@inject LabManagementClient Management
@inject ISnackbar Snackbar

<MudDialog>
Expand Down Expand Up @@ -118,9 +118,11 @@
private bool _saving;
private string? _error;

protected override async Task OnInitializedAsync()
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
await LoadAsync();
StateHasChanged();
}

private async Task LoadAsync()
Expand All @@ -130,7 +132,7 @@

try
{
var roles = await DemoRoleService.GetRolesAsync(UserKey);
var roles = await Management.SendAsync<List<string>>("/api/lab/manage/demo-roles/query", new { UserKey = UserKey.Value, Role = "", Assigned = false });

_manager = roles.Contains(
LabRoles.Manager,
Expand Down Expand Up @@ -165,8 +167,8 @@

try
{
await DemoRoleService.SetRoleAsync(
UserKey, role, assigned);
await Management.SendAsync<bool>("/api/lab/manage/demo-roles",
new { UserKey = UserKey.Value, Role = role, Assigned = assigned });

if (role == LabRoles.Manager)
_manager = assigned;
Expand Down
Loading
Loading