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);
|
private readonly record struct AuthOutcome(AuthenticatedUser? User, IResult? Failure);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decodes HTTP Basic Auth, binds against LDAP, and resolves roles — the same
|
/// Authenticates the audit-REST request via the shared
|
||||||
/// flow <see cref="ManagementEndpoints"/> uses. Returns a populated
|
/// <see cref="ManagementAuthenticator.AuthenticateAsync"/> — the dev/test
|
||||||
/// <see cref="AuthenticatedUser"/> on success, or an <see cref="IResult"/>
|
/// <c>DisableLogin</c> bypass first, otherwise HTTP Basic → LDAP → roles, the same flow
|
||||||
/// carrying the 401 response on any failure.
|
/// <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>
|
/// </summary>
|
||||||
private static async Task<AuthOutcome> AuthenticateAsync(HttpContext context)
|
private static async Task<AuthOutcome> AuthenticateAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
var authHeader = context.Request.Headers.Authorization.ToString();
|
var outcome = await ManagementAuthenticator.AuthenticateAsync(context);
|
||||||
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
return new AuthOutcome(outcome.User, outcome.Failure);
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasAnyRole(AuthenticatedUser user, string[] allowed) =>
|
private static bool HasAnyRole(AuthenticatedUser user, string[] allowed) =>
|
||||||
|
|||||||
@@ -75,6 +75,21 @@ public class DebugStreamHub : Hub
|
|||||||
return;
|
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)
|
||||||
|
{
|
||||||
|
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
|
// Extract Basic Auth credentials
|
||||||
var authHeader = httpContext.Request.Headers.Authorization.ToString();
|
var authHeader = httpContext.Request.Headers.Authorization.ToString();
|
||||||
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using ZB.MOM.WW.Auth.Abstractions.Ldap;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Security;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Security.Auth;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.ManagementService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared HTTP-Basic → LDAP authentication for the management/CLI surfaces
|
||||||
|
/// (<see cref="ManagementEndpoints"/>, <see cref="AuditEndpoints"/>, and the debug-stream hub).
|
||||||
|
/// Centralising the flow here means the dev/test <c>DisableLogin</c> bypass is applied
|
||||||
|
/// <em>identically</em> on every CLI-facing surface.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// <c>ScadaBridge:Security:Auth:DisableLogin</c> swaps the <em>cookie</em> authentication
|
||||||
|
/// scheme for <c>AutoLoginAuthenticationHandler</c>, which lets the interactive browser UI
|
||||||
|
/// in without credentials. The CLI does not use cookies — it authenticates the
|
||||||
|
/// <c>POST /management</c> (and audit REST) surfaces with HTTP Basic, which run their own
|
||||||
|
/// manual Basic → LDAP check. That check is independent of the cookie scheme, so without the
|
||||||
|
/// bypass below the CLI is locked out of a login-disabled deployment whenever LDAP is not in
|
||||||
|
/// use (the bind fails and every call returns <c>401 AUTH_FAILED</c>).
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The bypass mirrors <c>AutoLoginAuthenticationHandler</c> exactly: the configured dev user
|
||||||
|
/// (default <c>multi-role</c>) with ALL <see cref="Roles.All"/> roles, system-wide (empty
|
||||||
|
/// <see cref="AuthenticatedUser.PermittedSiteIds"/>). The startup <c>AUTH DISABLED</c> warning
|
||||||
|
/// (emitted by <c>AddSecurity</c>) already announces this posture. Dev/test ONLY.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static class ManagementAuthenticator
|
||||||
|
{
|
||||||
|
/// <summary>Outcome of <see cref="AuthenticateAsync"/>: exactly one of the two fields is set.</summary>
|
||||||
|
/// <param name="User">The authenticated principal on success; otherwise <see langword="null"/>.</param>
|
||||||
|
/// <param name="Failure">The 401 <see cref="IResult"/> to return on failure; otherwise <see langword="null"/>.</param>
|
||||||
|
public readonly record struct AuthOutcome(AuthenticatedUser? User, IResult? Failure);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When <c>ScadaBridge:Security:Auth:DisableLogin</c> is true, returns the synthesized dev
|
||||||
|
/// principal (configured user, ALL roles, system-wide) that mirrors the cookie-scheme
|
||||||
|
/// <c>AutoLoginAuthenticationHandler</c> — so the Basic-Auth CLI surfaces are reachable in a
|
||||||
|
/// login-disabled (e.g. no-LDAP) deployment. Returns <see langword="null"/> when the flag is
|
||||||
|
/// off, in which case the caller must fall back to real Basic → LDAP authentication.
|
||||||
|
/// Dev/test ONLY.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The current HTTP request context (used to resolve the options).</param>
|
||||||
|
/// <returns>The dev principal when login is disabled; otherwise <see langword="null"/>.</returns>
|
||||||
|
public static AuthenticatedUser? TryDisableLoginUser(HttpContext context)
|
||||||
|
{
|
||||||
|
var opts = context.RequestServices.GetService<IOptions<AuthDisableLoginOptions>>()?.Value;
|
||||||
|
if (opts is null || !opts.DisableLogin)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var user = string.IsNullOrWhiteSpace(opts.User) ? "multi-role" : opts.User;
|
||||||
|
return new AuthenticatedUser(user, user, Roles.All, Array.Empty<string>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticates a management/CLI request. Honours the dev/test <c>DisableLogin</c> bypass
|
||||||
|
/// first (<see cref="TryDisableLoginUser"/>); otherwise decodes HTTP Basic Auth, binds
|
||||||
|
/// against LDAP, and resolves roles. Returns a populated <see cref="AuthenticatedUser"/> on
|
||||||
|
/// success, or an <see cref="IResult"/> carrying the 401 response on any failure.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The HTTP request to authenticate.</param>
|
||||||
|
/// <returns>An <see cref="AuthOutcome"/> carrying either the user or the 401 failure result.</returns>
|
||||||
|
public static async Task<AuthOutcome> AuthenticateAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
// Dev/test bypass: when login is disabled, the CLI surface auto-authenticates as the
|
||||||
|
// configured dev user — no credential check — mirroring the interactive cookie handler.
|
||||||
|
var bypass = TryDisableLoginUser(context);
|
||||||
|
if (bypass is not null)
|
||||||
|
return new AuthOutcome(bypass, null);
|
||||||
|
|
||||||
|
// 1. Decode Basic Auth
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. LDAP authentication
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 resolvedUser = new AuthenticatedUser(
|
||||||
|
authResult.Username,
|
||||||
|
authResult.DisplayName,
|
||||||
|
mappingResult.Roles.ToArray(),
|
||||||
|
permittedSiteIds);
|
||||||
|
|
||||||
|
return new AuthOutcome(resolvedUser, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,55 +61,17 @@ public static class ManagementEndpoints
|
|||||||
maxBodyFeature.MaxRequestBodySize = MaxManagementRequestBodyBytes;
|
maxBodyFeature.MaxRequestBodySize = MaxManagementRequestBodyBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Decode Basic Auth
|
// 1-3. Authenticate: dev/test DisableLogin bypass → else HTTP Basic → LDAP → roles.
|
||||||
var authHeader = context.Request.Headers.Authorization.ToString();
|
// Centralised in ManagementAuthenticator so every CLI surface honours DisableLogin
|
||||||
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
// 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;
|
var authenticatedUser = auth.User!;
|
||||||
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);
|
|
||||||
|
|
||||||
// 4. Parse command from request body
|
// 4. Parse command from request body
|
||||||
string body;
|
string body;
|
||||||
|
|||||||
@@ -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