fix(management-service): resolve ManagementService-001/002/003 — enforce site scope on query/snapshot handlers and DebugStreamHub
This commit is contained in:
@@ -2,6 +2,7 @@ using System.Text;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ScadaLink.Commons.Interfaces.Repositories;
|
||||
using ScadaLink.Commons.Messages.DebugView;
|
||||
using ScadaLink.Commons.Messages.Streaming;
|
||||
using ScadaLink.Communication;
|
||||
@@ -17,6 +18,26 @@ namespace ScadaLink.ManagementService;
|
||||
public class DebugStreamHub : Hub
|
||||
{
|
||||
private const string SessionIdKey = "DebugStreamSessionId";
|
||||
private const string RolesKey = "DebugStreamRoles";
|
||||
private const string PermittedSiteIdsKey = "DebugStreamPermittedSiteIds";
|
||||
|
||||
/// <summary>
|
||||
/// Pure site-scope authorization check for a debug-stream subscription.
|
||||
/// Returns true when the caller may subscribe to a debug stream for an instance
|
||||
/// belonging to <paramref name="instanceSiteId"/>.
|
||||
/// Admin role, or an empty <paramref name="permittedSiteIds"/> (system-wide
|
||||
/// Deployment), grants access to any site; otherwise the instance's site must be
|
||||
/// in the permitted set.
|
||||
/// </summary>
|
||||
public static bool IsInstanceAccessAllowed(
|
||||
IReadOnlyCollection<string> roles,
|
||||
IReadOnlyCollection<string> permittedSiteIds,
|
||||
int instanceSiteId)
|
||||
{
|
||||
if (roles.Contains("Admin", StringComparer.OrdinalIgnoreCase)) return true;
|
||||
if (permittedSiteIds.Count == 0) return true; // system-wide deployment
|
||||
return permittedSiteIds.Contains(instanceSiteId.ToString());
|
||||
}
|
||||
|
||||
private readonly DebugStreamService _debugStreamService;
|
||||
private readonly IHubContext<DebugStreamHub> _hubContext;
|
||||
@@ -93,6 +114,11 @@ public class DebugStreamHub : Hub
|
||||
return;
|
||||
}
|
||||
|
||||
// Persist the resolved identity on the connection so per-instance site-scope
|
||||
// enforcement can be applied to SubscribeInstance calls.
|
||||
Context.Items[RolesKey] = mappingResult.Roles.ToArray();
|
||||
Context.Items[PermittedSiteIdsKey] = mappingResult.PermittedSiteIds.ToArray();
|
||||
|
||||
_logger.LogInformation("DebugStreamHub connection established for {Username}", username);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
@@ -108,6 +134,41 @@ public class DebugStreamHub : Hub
|
||||
|
||||
var connectionId = Context.ConnectionId;
|
||||
|
||||
// Per-instance site-scope enforcement: a site-scoped Deployment user must not
|
||||
// be able to stream an instance belonging to a site outside their scope.
|
||||
var httpContext = Context.GetHttpContext();
|
||||
if (httpContext == null)
|
||||
{
|
||||
_logger.LogWarning("DebugStreamHub: {ConnectionId} subscribe rejected — no HTTP context", connectionId);
|
||||
await Clients.Caller.SendAsync("OnStreamTerminated", "Authorization context unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
var roles = Context.Items.TryGetValue(RolesKey, out var rolesObj) && rolesObj is string[] r
|
||||
? r : Array.Empty<string>();
|
||||
var permittedSiteIds = Context.Items.TryGetValue(PermittedSiteIdsKey, out var sitesObj) && sitesObj is string[] s
|
||||
? s : Array.Empty<string>();
|
||||
|
||||
var instanceRepo = httpContext.RequestServices.GetRequiredService<ITemplateEngineRepository>();
|
||||
var instance = await instanceRepo.GetInstanceByIdAsync(instanceId);
|
||||
if (instance == null)
|
||||
{
|
||||
_logger.LogWarning("DebugStreamHub: {ConnectionId} subscribe rejected — instance {InstanceId} not found",
|
||||
connectionId, instanceId);
|
||||
await Clients.Caller.SendAsync("OnStreamTerminated", $"Instance {instanceId} not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsInstanceAccessAllowed(roles, permittedSiteIds, instance.SiteId))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"DebugStreamHub: {ConnectionId} subscribe to instance {InstanceId} denied — site {SiteId} outside permitted scope",
|
||||
connectionId, instanceId, instance.SiteId);
|
||||
await Clients.Caller.SendAsync("OnStreamTerminated",
|
||||
$"Access denied: instance {instanceId} belongs to a site outside your Deployment scope.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Use IHubContext for callbacks — the hub instance is transient (disposed after method returns),
|
||||
|
||||
Reference in New Issue
Block a user