Files
Ai/TrafagSalesExporter/Services/FinanceCockpitAccessService.cs

145 lines
5.3 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using TrafagSalesExporter.Security;
namespace TrafagSalesExporter.Services;
public interface IFinanceCockpitAccessService
{
bool IsEnabled { get; }
bool IsConfigured { get; }
bool IsUnlocked { get; }
bool TryUnlock(string username, string password);
bool TryChangePassword(string username, string currentPassword, string newPassword);
void Lock();
}
public sealed class FinanceCockpitAccessService : IFinanceCockpitAccessService, IDisposable
{
private readonly FinanceCockpitAccessOptions _options;
private readonly IHostEnvironment _environment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IAccessSessionTracker _sessionTracker;
private readonly ILogger<FinanceCockpitAccessService> _logger;
private readonly string _sessionId = Guid.NewGuid().ToString("N");
public FinanceCockpitAccessService(
IOptions<FinanceCockpitAccessOptions> options,
IHostEnvironment environment,
IHttpContextAccessor httpContextAccessor,
IAccessSessionTracker sessionTracker,
ILogger<FinanceCockpitAccessService> logger)
{
_options = options.Value;
_environment = environment;
_httpContextAccessor = httpContextAccessor;
_sessionTracker = sessionTracker;
_logger = logger;
}
public bool IsEnabled => _options.Enabled;
public bool IsConfigured =>
!IsEnabled ||
!string.IsNullOrWhiteSpace(_options.Username) &&
(!string.IsNullOrWhiteSpace(_options.PasswordHash) || !string.IsNullOrEmpty(_options.Password));
public bool IsUnlocked =>
_isUnlocked ||
AccessUnlockCookie.IsUnlocked(
_httpContextAccessor.HttpContext,
AccessUnlockCookie.FinanceCookieName,
_options.PasswordHash);
private bool _isUnlocked;
public bool TryUnlock(string username, string password)
{
if (!IsEnabled)
{
_isUnlocked = true;
_logger.LogInformation("Finance Cockpit access unlocked because FinanceCockpitAccess is disabled.");
return true;
}
if (!IsConfigured ||
string.IsNullOrWhiteSpace(username) ||
string.IsNullOrEmpty(password) ||
!FixedEquals(username.Trim(), _options.Username.Trim()))
{
_logger.LogWarning(
"Finance Cockpit unlock failed before password check. IsConfigured={IsConfigured}, HasUsername={HasUsername}, PasswordLength={PasswordLength}, UsernameMatches={UsernameMatches}",
IsConfigured,
!string.IsNullOrWhiteSpace(username),
password?.Length ?? 0,
!string.IsNullOrWhiteSpace(username) && FixedEquals(username.Trim(), _options.Username.Trim()));
return false;
}
var valid = !string.IsNullOrWhiteSpace(_options.PasswordHash)
? VerifyPasswordHash(password, _options.PasswordHash)
: FixedEquals(password, _options.Password);
_isUnlocked = valid;
_logger.Log(
valid ? LogLevel.Information : LogLevel.Warning,
"Finance Cockpit password check completed. Success={Success}, Username={Username}, PasswordLength={PasswordLength}, UsesHash={UsesHash}",
valid,
username.Trim(),
password.Length,
!string.IsNullOrWhiteSpace(_options.PasswordHash));
if (valid)
_sessionTracker.Register(_sessionId, "Finance Cockpit", username.Trim(), GetRemoteAddress());
return valid;
}
public void Lock()
{
_isUnlocked = false;
_sessionTracker.Unregister(_sessionId);
}
public bool TryChangePassword(string username, string currentPassword, string newPassword)
{
if (!IsEnabled ||
!IsConfigured ||
string.IsNullOrWhiteSpace(newPassword) ||
newPassword.Length < 8 ||
!TryUnlock(username, currentPassword))
{
return false;
}
var passwordHash = AccessPasswordSettingsWriter.HashPassword(newPassword);
AccessPasswordSettingsWriter.SavePasswordHash(_environment.ContentRootPath, FinanceCockpitAccessOptions.SectionName, passwordHash);
_options.PasswordHash = passwordHash;
_options.Password = string.Empty;
_isUnlocked = true;
_sessionTracker.Register(_sessionId, "Finance Cockpit", username.Trim(), GetRemoteAddress());
return true;
}
public void Dispose()
{
_sessionTracker.Unregister(_sessionId);
}
private string? GetRemoteAddress()
=> _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString();
private static bool VerifyPasswordHash(string password, string configuredHash)
{
var passwordHash = AccessPasswordSettingsWriter.HashPassword(password);
return FixedEquals(passwordHash, configuredHash.Trim());
}
private static bool FixedEquals(string left, string right)
{
var leftBytes = Encoding.UTF8.GetBytes(left);
var rightBytes = Encoding.UTF8.GetBytes(right);
return leftBytes.Length == rightBytes.Length &&
CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes);
}
}