fix(management): honor DisableLogin on the Basic-Auth CLI surfaces
DisableLogin only swapped the cookie auth scheme (AutoLoginAuthenticationHandler), which covers the interactive UI. The CLI authenticates POST /management, the audit REST endpoints, and the SignalR debug-stream hub with HTTP Basic, and each ran its own hardcoded Basic->LDAP check that ignored DisableLogin. In a login-disabled (e.g. no-LDAP) deployment that locked the CLI out: every call returned 401 AUTH_FAILED. Add ManagementAuthenticator, which centralizes the management/CLI auth flow: when ScadaBridge:Security:Auth:DisableLogin is true it synthesizes the same dev principal as AutoLoginAuthenticationHandler (configured user, all roles, system-wide) and bypasses Basic->LDAP; otherwise the unchanged Basic->LDAP flow runs. Wired into ManagementEndpoints (delegates), AuditEndpoints (delegates), and DebugStreamHub (bypass branch). +6 unit tests; ManagementService.Tests green (140).
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.ManagementService;
|
||||
using ZB.MOM.WW.ScadaBridge.Security;
|
||||
using ZB.MOM.WW.ScadaBridge.Security.Auth;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for the dev/test <c>DisableLogin</c> bypass on the management/CLI auth surface
|
||||
/// (<see cref="ManagementAuthenticator"/>). The bypass lets the HTTP-Basic CLI surfaces
|
||||
/// authenticate without LDAP when login is disabled — mirroring the interactive cookie
|
||||
/// handler — so a login-disabled deployment is not locked out of the CLI.
|
||||
/// </summary>
|
||||
public class ManagementAuthenticatorTests
|
||||
{
|
||||
private static HttpContext ContextWith(AuthDisableLoginOptions options)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IOptions<AuthDisableLoginOptions>>(Options.Create(options));
|
||||
return new DefaultHttpContext { RequestServices = services.BuildServiceProvider() };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDisableLoginUser_WhenDisabled_ReturnsNull()
|
||||
{
|
||||
var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = false });
|
||||
|
||||
Assert.Null(ManagementAuthenticator.TryDisableLoginUser(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDisableLoginUser_WhenNoOptionsRegistered_ReturnsNull()
|
||||
{
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
RequestServices = new ServiceCollection().BuildServiceProvider(),
|
||||
};
|
||||
|
||||
Assert.Null(ManagementAuthenticator.TryDisableLoginUser(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDisableLoginUser_WhenEnabled_ReturnsDevUserWithAllRolesSystemWide()
|
||||
{
|
||||
var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = true, User = "ci-bot" });
|
||||
|
||||
var user = ManagementAuthenticator.TryDisableLoginUser(context);
|
||||
|
||||
Assert.NotNull(user);
|
||||
Assert.Equal("ci-bot", user!.Username);
|
||||
Assert.Equal("ci-bot", user.DisplayName);
|
||||
Assert.Equal(Roles.All, user.Roles);
|
||||
Assert.Empty(user.PermittedSiteIds); // empty == system-wide, mirrors AutoLoginAuthenticationHandler
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryDisableLoginUser_WhenEnabledWithBlankUser_FallsBackToMultiRole()
|
||||
{
|
||||
var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = true, User = " " });
|
||||
|
||||
var user = ManagementAuthenticator.TryDisableLoginUser(context);
|
||||
|
||||
Assert.NotNull(user);
|
||||
Assert.Equal("multi-role", user!.Username);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_WhenDisabled_BypassesBasicAuthAndReturnsDevUser()
|
||||
{
|
||||
// No Authorization header and no ILdapAuthService registered: the bypass must short-circuit
|
||||
// before either is consulted.
|
||||
var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = true });
|
||||
|
||||
var outcome = await ManagementAuthenticator.AuthenticateAsync(context);
|
||||
|
||||
Assert.Null(outcome.Failure);
|
||||
Assert.NotNull(outcome.User);
|
||||
Assert.Equal("multi-role", outcome.User!.Username);
|
||||
Assert.Equal(Roles.All, outcome.User.Roles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AuthenticateAsync_WhenEnabledWithNoHeader_ReturnsUnauthorized()
|
||||
{
|
||||
// Login NOT disabled and no Basic header: the real auth path rejects with a 401 result
|
||||
// before any LDAP lookup.
|
||||
var context = ContextWith(new AuthDisableLoginOptions { DisableLogin = false });
|
||||
|
||||
var outcome = await ManagementAuthenticator.AuthenticateAsync(context);
|
||||
|
||||
Assert.Null(outcome.User);
|
||||
Assert.NotNull(outcome.Failure);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user