using System.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Server.Configuration; namespace ZB.MOM.WW.MxGateway.Server.Dashboard; /// /// Authorizes a dashboard request by checking either: (a) the LDAP-issued /// role claim satisfies , /// (b) authentication is fully disabled, or (c) the request is from loopback /// and MxGateway:Dashboard:AllowAnonymousLocalhost is on. /// /// /// The environment bypasses in (b) and (c) grant read-only access only: they /// satisfy a requirement that includes (i.e. /// ) but never the /// requirement. Destructive/admin /// surfaces still require a real Admin role claim, preserving the documented /// "anonymous localhost is read-only" contract at the policy layer rather than by accident /// in downstream service re-checks. The loopback test trusts /// Connection.RemoteIpAddress; if forwarded-headers middleware is ever added upstream, /// must be revisited so a spoofed X-Forwarded-For /// cannot masquerade as loopback. /// public sealed class DashboardAuthorizationHandler( IHttpContextAccessor httpContextAccessor, IOptions options) : AuthorizationHandler { /// protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, DashboardAuthorizationRequirement requirement) { GatewayOptions gatewayOptions = options.Value; // Environment bypasses are read-only: they satisfy the Viewer requirement // (AnyDashboardRole) but never AdminOnly, which lacks Viewer in RequiredRoles. bool grantsReadOnly = requirement.RequiredRoles.Contains(DashboardRoles.Viewer); if (grantsReadOnly && gatewayOptions.Authentication.Mode == AuthenticationMode.Disabled) { context.Succeed(requirement); return Task.CompletedTask; } if (grantsReadOnly && gatewayOptions.Dashboard.AllowAnonymousLocalhost && IsLoopbackRequest()) { context.Succeed(requirement); return Task.CompletedTask; } if (context.User.Identity?.IsAuthenticated != true) { return Task.CompletedTask; } foreach (string role in requirement.RequiredRoles) { if (context.User.IsInRole(role)) { context.Succeed(requirement); return Task.CompletedTask; } } return Task.CompletedTask; } private bool IsLoopbackRequest() { IPAddress? remoteAddress = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress; return remoteAddress is not null && IPAddress.IsLoopback(remoteAddress); } }