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:
@@ -329,63 +329,16 @@ public static class AuditEndpoints
|
||||
private readonly record struct AuthOutcome(AuthenticatedUser? User, IResult? Failure);
|
||||
|
||||
/// <summary>
|
||||
/// Decodes HTTP Basic Auth, binds against LDAP, and resolves roles — the same
|
||||
/// flow <see cref="ManagementEndpoints"/> uses. Returns a populated
|
||||
/// <see cref="AuthenticatedUser"/> on success, or an <see cref="IResult"/>
|
||||
/// carrying the 401 response on any failure.
|
||||
/// Authenticates the audit-REST request via the shared
|
||||
/// <see cref="ManagementAuthenticator.AuthenticateAsync"/> — the dev/test
|
||||
/// <c>DisableLogin</c> bypass first, otherwise HTTP Basic → LDAP → roles, the same flow
|
||||
/// <see cref="ManagementEndpoints"/> uses. Returns a populated <see cref="AuthenticatedUser"/>
|
||||
/// on success, or an <see cref="IResult"/> carrying the 401 response on any failure.
|
||||
/// </summary>
|
||||
private static async Task<AuthOutcome> AuthenticateAsync(HttpContext context)
|
||||
{
|
||||
var authHeader = context.Request.Headers.Authorization.ToString();
|
||||
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new AuthOutcome(null, Results.Json(
|
||||
new { error = "Authorization header required (Basic scheme).", code = "AUTH_FAILED" }, statusCode: 401));
|
||||
}
|
||||
|
||||
string username, password;
|
||||
try
|
||||
{
|
||||
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
|
||||
{
|
||||
return new AuthOutcome(null, Results.Json(
|
||||
new { error = "Malformed Basic Auth header.", code = "AUTH_FAILED" }, statusCode: 401));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return new AuthOutcome(null, Results.Json(
|
||||
new { error = "Username and password are required.", code = "AUTH_FAILED" }, statusCode: 401));
|
||||
}
|
||||
|
||||
var ldapAuth = context.RequestServices.GetRequiredService<ILdapAuthService>();
|
||||
var authResult = await ldapAuth.AuthenticateAsync(username, password, context.RequestAborted);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
return new AuthOutcome(null, Results.Json(
|
||||
new { error = LdapAuthFailureMessages.ToMessage(authResult.Failure), code = "AUTH_FAILED" }, statusCode: 401));
|
||||
}
|
||||
|
||||
var roleMapper = context.RequestServices.GetRequiredService<RoleMapper>();
|
||||
var mappingResult = await roleMapper.MapGroupsToRolesAsync(authResult.Groups, context.RequestAborted);
|
||||
|
||||
var permittedSiteIds = mappingResult.IsSystemWideDeployment
|
||||
? Array.Empty<string>()
|
||||
: mappingResult.PermittedSiteIds.ToArray();
|
||||
|
||||
var user = new AuthenticatedUser(
|
||||
authResult.Username,
|
||||
authResult.DisplayName,
|
||||
mappingResult.Roles.ToArray(),
|
||||
permittedSiteIds);
|
||||
|
||||
return new AuthOutcome(user, null);
|
||||
var outcome = await ManagementAuthenticator.AuthenticateAsync(context);
|
||||
return new AuthOutcome(outcome.User, outcome.Failure);
|
||||
}
|
||||
|
||||
private static bool HasAnyRole(AuthenticatedUser user, string[] allowed) =>
|
||||
|
||||
Reference in New Issue
Block a user