fix(security): throttle DebugStreamHub LDAP bind via ManagementAuthenticator + correct coverage doc (plan R2-07 T1)
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -6,7 +5,6 @@ using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
using ZB.MOM.WW.Auth.Abstractions.Ldap;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ManagementService;
|
||||
@@ -75,73 +73,35 @@ public class DebugStreamHub : Hub
|
||||
return;
|
||||
}
|
||||
|
||||
// Dev/test: when login is disabled, auto-authenticate the debug stream as the configured
|
||||
// dev user (ALL roles incl. Deployer, system-wide) — mirroring the cookie/management
|
||||
// bypass so the CLI debug stream is usable in a login-disabled (e.g. no-LDAP) deployment.
|
||||
// See ManagementAuthenticator.
|
||||
var bypassUser = ManagementAuthenticator.TryDisableLoginUser(httpContext);
|
||||
if (bypassUser is not null)
|
||||
// Authenticate via the SAME shared Basic→LDAP flow as POST /management and the
|
||||
// audit REST (arch-review R2 N1): ManagementAuthenticator applies the DisableLogin
|
||||
// bypass, the LoginThrottle lockout check BEFORE the bind, and RecordFailure/
|
||||
// RecordSuccess bookkeeping — so hub connection attempts can no longer be used as
|
||||
// an unthrottled LDAP-bind oracle, and hub failures feed the shared lockout.
|
||||
var outcome = await ManagementAuthenticator.AuthenticateAsync(httpContext);
|
||||
if (outcome.User is null)
|
||||
{
|
||||
Context.Items[RolesKey] = bypassUser.Roles;
|
||||
Context.Items[PermittedSiteIdsKey] = bypassUser.PermittedSiteIds;
|
||||
_logger.LogInformation(
|
||||
"DebugStreamHub connection established for {Username} (login disabled)", bypassUser.Username);
|
||||
await base.OnConnectedAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract Basic Auth credentials
|
||||
var authHeader = httpContext.Request.Headers.Authorization.ToString();
|
||||
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning("DebugStreamHub connection rejected: missing Basic Auth header");
|
||||
_logger.LogWarning("DebugStreamHub connection rejected: authentication failed or throttled");
|
||||
Context.Abort();
|
||||
return;
|
||||
}
|
||||
var user = outcome.User;
|
||||
|
||||
string username, password;
|
||||
try
|
||||
// Role check — Deployer role required (unchanged policy).
|
||||
if (!user.Roles.Contains(Roles.Deployer))
|
||||
{
|
||||
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader["Basic ".Length..]));
|
||||
var colon = decoded.IndexOf(':');
|
||||
if (colon < 0) throw new FormatException();
|
||||
username = decoded[..colon];
|
||||
password = decoded[(colon + 1)..];
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogWarning("DebugStreamHub connection rejected: malformed Basic Auth");
|
||||
Context.Abort();
|
||||
return;
|
||||
}
|
||||
|
||||
// LDAP authentication
|
||||
var ldapAuth = httpContext.RequestServices.GetRequiredService<ILdapAuthService>();
|
||||
var authResult = await ldapAuth.AuthenticateAsync(username, password, Context.ConnectionAborted);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
_logger.LogWarning("DebugStreamHub connection rejected: LDAP auth failed for {Username}", username);
|
||||
Context.Abort();
|
||||
return;
|
||||
}
|
||||
|
||||
// Role check — Deployer role required
|
||||
var roleMapper = httpContext.RequestServices.GetRequiredService<RoleMapper>();
|
||||
var mappingResult = await roleMapper.MapGroupsToRolesAsync(authResult.Groups, Context.ConnectionAborted);
|
||||
|
||||
if (!mappingResult.Roles.Contains(Roles.Deployer))
|
||||
{
|
||||
_logger.LogWarning("DebugStreamHub connection rejected: {Username} lacks Deployer role", username);
|
||||
_logger.LogWarning("DebugStreamHub connection rejected: {Username} lacks Deployer role", user.Username);
|
||||
Context.Abort();
|
||||
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();
|
||||
// enforcement can be applied to SubscribeInstance calls. PermittedSiteIds is the
|
||||
// authenticator's normalized form (empty == system-wide), same as every other surface.
|
||||
Context.Items[RolesKey] = user.Roles;
|
||||
Context.Items[PermittedSiteIdsKey] = user.PermittedSiteIds;
|
||||
|
||||
_logger.LogInformation("DebugStreamHub connection established for {Username}", username);
|
||||
_logger.LogInformation("DebugStreamHub connection established for {Username}", user.Username);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user