- WP-1-3: Central/site failover + dual-node recovery tests (17 tests) - WP-4: Performance testing framework for target scale (7 tests) - WP-5: Security hardening (LDAPS, JWT key length, no secrets in logs) (11 tests) - WP-6: Script sandboxing adversarial tests (28 tests, all forbidden APIs) - WP-7: Recovery drill test scaffolds (5 tests) - WP-8: Observability validation (structured logs, correlation IDs, metrics) (6 tests) - WP-9: Message contract compatibility (forward/backward compat) (18 tests) - WP-10: Deployment packaging (installation guide, production checklist, topology) - WP-11: Operational runbooks (failover, troubleshooting, maintenance) 92 new tests, all passing. Zero warnings.
81 lines
2.8 KiB
C#
81 lines
2.8 KiB
C#
using ScadaLink.Commons.Entities.InboundApi;
|
|
using ScadaLink.Commons.Interfaces.Repositories;
|
|
|
|
namespace ScadaLink.InboundAPI;
|
|
|
|
/// <summary>
|
|
/// WP-1: Validates API keys from X-API-Key header.
|
|
/// Checks that the key exists, is enabled, and is approved for the requested method.
|
|
/// </summary>
|
|
public class ApiKeyValidator
|
|
{
|
|
private readonly IInboundApiRepository _repository;
|
|
|
|
public ApiKeyValidator(IInboundApiRepository repository)
|
|
{
|
|
_repository = repository;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates an API key for a given method.
|
|
/// Returns (isValid, apiKey, statusCode, errorMessage).
|
|
/// </summary>
|
|
public async Task<ApiKeyValidationResult> ValidateAsync(
|
|
string? apiKeyValue,
|
|
string methodName,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (string.IsNullOrEmpty(apiKeyValue))
|
|
{
|
|
return ApiKeyValidationResult.Unauthorized("Missing X-API-Key header");
|
|
}
|
|
|
|
var apiKey = await _repository.GetApiKeyByValueAsync(apiKeyValue, cancellationToken);
|
|
if (apiKey == null || !apiKey.IsEnabled)
|
|
{
|
|
return ApiKeyValidationResult.Unauthorized("Invalid or disabled API key");
|
|
}
|
|
|
|
var method = await _repository.GetMethodByNameAsync(methodName, cancellationToken);
|
|
if (method == null)
|
|
{
|
|
return ApiKeyValidationResult.NotFound($"Method '{methodName}' not found");
|
|
}
|
|
|
|
// Check if this key is approved for the method
|
|
var approvedKeys = await _repository.GetApprovedKeysForMethodAsync(method.Id, cancellationToken);
|
|
var isApproved = approvedKeys.Any(k => k.Id == apiKey.Id);
|
|
|
|
if (!isApproved)
|
|
{
|
|
return ApiKeyValidationResult.Forbidden("API key not approved for this method");
|
|
}
|
|
|
|
return ApiKeyValidationResult.Valid(apiKey, method);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result of API key validation.
|
|
/// </summary>
|
|
public class ApiKeyValidationResult
|
|
{
|
|
public bool IsValid { get; private init; }
|
|
public int StatusCode { get; private init; }
|
|
public string? ErrorMessage { get; private init; }
|
|
public ApiKey? ApiKey { get; private init; }
|
|
public ApiMethod? Method { get; private init; }
|
|
|
|
public static ApiKeyValidationResult Valid(ApiKey apiKey, ApiMethod method) =>
|
|
new() { IsValid = true, StatusCode = 200, ApiKey = apiKey, Method = method };
|
|
|
|
public static ApiKeyValidationResult Unauthorized(string message) =>
|
|
new() { IsValid = false, StatusCode = 401, ErrorMessage = message };
|
|
|
|
public static ApiKeyValidationResult Forbidden(string message) =>
|
|
new() { IsValid = false, StatusCode = 403, ErrorMessage = message };
|
|
|
|
public static ApiKeyValidationResult NotFound(string message) =>
|
|
new() { IsValid = false, StatusCode = 400, ErrorMessage = message };
|
|
}
|