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:
@@ -61,55 +61,17 @@ public static class ManagementEndpoints
|
||||
maxBodyFeature.MaxRequestBodySize = MaxManagementRequestBodyBytes;
|
||||
}
|
||||
|
||||
// 1. Decode Basic Auth
|
||||
var authHeader = context.Request.Headers.Authorization.ToString();
|
||||
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
||||
// 1-3. Authenticate: dev/test DisableLogin bypass → else HTTP Basic → LDAP → roles.
|
||||
// Centralised in ManagementAuthenticator so every CLI surface honours DisableLogin
|
||||
// identically — the cookie auto-login (AutoLoginAuthenticationHandler) only covers the
|
||||
// interactive UI, not this Basic-Auth surface.
|
||||
var auth = await ManagementAuthenticator.AuthenticateAsync(context);
|
||||
if (auth.Failure is not null)
|
||||
{
|
||||
return Results.Json(new { error = "Authorization header required (Basic scheme).", code = "AUTH_FAILED" }, statusCode: 401);
|
||||
return auth.Failure;
|
||||
}
|
||||
|
||||
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 Results.Json(new { error = "Malformed Basic Auth header.", code = "AUTH_FAILED" }, statusCode: 401);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return Results.Json(new { error = "Username and password are required.", code = "AUTH_FAILED" }, statusCode: 401);
|
||||
}
|
||||
|
||||
// 2. LDAP authentication
|
||||
var ldapAuth = context.RequestServices.GetRequiredService<ILdapAuthService>();
|
||||
var authResult = await ldapAuth.AuthenticateAsync(username, password, context.RequestAborted);
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
return Results.Json(
|
||||
new { error = LdapAuthFailureMessages.ToMessage(authResult.Failure), code = "AUTH_FAILED" },
|
||||
statusCode: 401);
|
||||
}
|
||||
|
||||
// 3. Role resolution
|
||||
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 authenticatedUser = new AuthenticatedUser(
|
||||
authResult.Username,
|
||||
authResult.DisplayName,
|
||||
mappingResult.Roles.ToArray(),
|
||||
permittedSiteIds);
|
||||
var authenticatedUser = auth.User!;
|
||||
|
||||
// 4. Parse command from request body
|
||||
string body;
|
||||
|
||||
Reference in New Issue
Block a user