Phase 8: Production readiness — failover tests, security hardening, sandboxing, deployment docs
- 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.
This commit is contained in:
80
src/ScadaLink.InboundAPI/ApiKeyValidator.cs
Normal file
80
src/ScadaLink.InboundAPI/ApiKeyValidator.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user