fix(central-ui): resolve CentralUI-002/003/004 — site-scope enforcement, per-circuit console capture, cached auth state

This commit is contained in:
Joseph Doherty
2026-05-16 19:33:09 -04:00
parent 5a08b04535
commit 87f14c190a
17 changed files with 693 additions and 40 deletions

View File

@@ -11,6 +11,7 @@
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ISiteRepository SiteRepository
@inject ScadaLink.CentralUI.Auth.SiteScopeService SiteScope
@inject DebugStreamService DebugStreamService
@inject IJSRuntime JS
@implements IDisposable
@@ -296,7 +297,9 @@
{
try
{
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
// Site scoping (CentralUI-002): a scoped Deployment user may only
// debug sites they are permitted on.
_sites = await SiteScope.FilterSitesAsync(await SiteRepository.GetAllSitesAsync());
}
catch (Exception ex)
{
@@ -358,6 +361,14 @@
_siteInstances.Clear();
_selectedInstanceId = 0;
if (_selectedSiteId == 0) return;
// Site scoping (CentralUI-002): re-check the claim server-side — a query
// string or stale localStorage value could name a site outside the grant.
if (!await SiteScope.IsSiteAllowedAsync(_selectedSiteId))
{
_selectedSiteId = 0;
_toast.ShowError("You are not permitted to debug instances on that site.");
return;
}
try
{
_siteInstances = (await TemplateEngineRepository.GetInstancesBySiteIdAsync(_selectedSiteId))

View File

@@ -7,6 +7,7 @@
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
@inject IDeploymentManagerRepository DeploymentManagerRepository
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ScadaLink.CentralUI.Auth.SiteScopeService SiteScope
@implements IDisposable
<div class="container-fluid mt-3">
@@ -245,13 +246,23 @@
_errorMessage = null;
try
{
_records = (await DeploymentManagerRepository.GetAllDeploymentRecordsAsync())
.OrderByDescending(r => r.DeployedAt)
.ToList();
// Build instance name lookup
// Build instance lookups first — site scoping (CentralUI-002) filters
// deployment records by the site of their instance.
var instances = await TemplateEngineRepository.GetAllInstancesAsync();
_instanceNames = instances.ToDictionary(i => i.Id, i => i.UniqueName);
var instanceSiteIds = instances.ToDictionary(i => i.Id, i => i.SiteId);
var systemWide = await SiteScope.IsSystemWideAsync();
var permittedSiteIds = systemWide
? null
: await SiteScope.PermittedSiteIdsAsync();
_records = (await DeploymentManagerRepository.GetAllDeploymentRecordsAsync())
.Where(r => permittedSiteIds == null
|| (instanceSiteIds.TryGetValue(r.InstanceId, out var sid)
&& permittedSiteIds.Contains(sid)))
.OrderByDescending(r => r.DeployedAt)
.ToList();
_totalPages = Math.Max(1, (int)Math.Ceiling(_records.Count / (double)PageSize));
if (_currentPage > _totalPages) _currentPage = 1;

View File

@@ -11,6 +11,7 @@
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ISiteRepository SiteRepository
@inject ScadaLink.CentralUI.Auth.SiteScopeService SiteScope
@inject InstanceService InstanceService
@inject IFlatteningPipeline FlatteningPipeline
@inject AuthenticationStateProvider AuthStateProvider
@@ -377,6 +378,17 @@
return;
}
// Site scoping (CentralUI-002): a scoped Deployment user must not be
// able to configure or deploy an instance on a site outside their
// grant by navigating straight to its URL.
if (!await SiteScope.IsSiteAllowedAsync(_instance.SiteId))
{
_instance = null;
_errorMessage = "You are not permitted to manage instances on this site.";
_loading = false;
return;
}
// Identity
var template = await TemplateEngineRepository.GetTemplateByIdAsync(_instance.TemplateId);
_templateName = template?.Name ?? $"#{_instance.TemplateId}";

View File

@@ -8,6 +8,7 @@
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ISiteRepository SiteRepository
@inject ScadaLink.CentralUI.Auth.SiteScopeService SiteScope
@inject InstanceService InstanceService
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@@ -93,7 +94,9 @@
try
{
_templates = (await TemplateEngineRepository.GetAllTemplatesAsync()).ToList();
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
// Site scoping (CentralUI-002): a scoped Deployment user may only
// create instances on sites they are permitted on.
_sites = await SiteScope.FilterSitesAsync(await SiteRepository.GetAllSitesAsync());
_allAreas.Clear();
foreach (var site in _sites)
@@ -124,6 +127,13 @@
if (string.IsNullOrWhiteSpace(_createName)) { _formError = "Instance name is required."; return; }
if (_createTemplateId == 0) { _formError = "Select a template."; return; }
if (_createSiteId == 0) { _formError = "Select a site."; return; }
// Site scoping (CentralUI-002): re-check server-side before the mutating
// command, independent of what the site dropdown was populated with.
if (!await SiteScope.IsSiteAllowedAsync(_createSiteId))
{
_formError = "You are not permitted to create instances on the selected site.";
return;
}
try
{

View File

@@ -17,6 +17,7 @@
@inject AreaService AreaService
@inject InstanceService InstanceService
@inject AuthenticationStateProvider AuthStateProvider
@inject ScadaLink.CentralUI.Auth.SiteScopeService SiteScope
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@inject IDialogService Dialog
@@ -225,8 +226,13 @@
_errorMessage = null;
try
{
_allInstances = (await TemplateEngineRepository.GetAllInstancesAsync()).ToList();
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
// Site scoping (CentralUI-002): a scoped Deployment user only sees the
// sites — and therefore the areas/instances — they are permitted on.
_sites = await SiteScope.FilterSitesAsync(await SiteRepository.GetAllSitesAsync());
var permittedSiteIds = _sites.Select(s => s.Id).ToHashSet();
_allInstances = (await TemplateEngineRepository.GetAllInstancesAsync())
.Where(i => permittedSiteIds.Contains(i.SiteId))
.ToList();
_templates = (await TemplateEngineRepository.GetAllTemplatesAsync()).ToList();
_allAreas.Clear();