w4c-workflows-api/Controllers/WorkflowQuotaController.cs

82 lines
2.7 KiB
C#
Raw Normal View History

2026-09-11 22:02:46 +00:00
using Microsoft.AspNetCore.Mvc;
using w4c_workflows.Filters;
using w4c_workflows.Services.Quota;
namespace w4c_workflows.Controllers;
/// <summary>
/// Per-tenant workflow execution quota. <c>GET</c> returns the current period's
2026-09-13 08:35:17 +00:00
/// usage (drives the counter on the Workflows page). <c>POST reset</c> is a
/// platform-admin action: a tenant must not be able to zero its own counter, so
/// it additionally requires the shared <c>X-Admin-Key</c> matching
/// <c>Workflows:AdminApiKey</c> (fail-closed when unset).
2026-09-11 22:02:46 +00:00
/// </summary>
[ApiController]
[Route("api/workflows/quota")]
public class WorkflowQuotaController : ControllerBase
{
private readonly WorkflowQuotaService _quota;
2026-09-13 08:35:17 +00:00
private readonly string? _adminKey;
2026-09-11 22:02:46 +00:00
2026-09-13 08:35:17 +00:00
public WorkflowQuotaController(WorkflowQuotaService quota, IConfiguration config)
2026-09-11 22:02:46 +00:00
{
_quota = quota;
2026-09-13 08:35:17 +00:00
_adminKey = string.IsNullOrWhiteSpace(config["Workflows:AdminApiKey"])
? null
: config["Workflows:AdminApiKey"];
2026-09-11 22:02:46 +00:00
}
private string TenantId => (string?)HttpContext.Items["TenantId"]
?? throw new InvalidOperationException("TenantId not resolved by auth middleware");
/// <summary>Current execution usage against this tenant's monthly limit.</summary>
[HttpGet]
[RequireScope("read")]
public async Task<IActionResult> Get(CancellationToken ct)
{
return Ok(await _quota.GetAsync(TenantId, ct));
}
2026-09-13 08:35:17 +00:00
/// <summary>
/// Resets the current period's execution counter to zero. Platform admin only:
/// requires the shared admin key in addition to the operator key.
/// </summary>
2026-09-11 22:02:46 +00:00
[HttpPost("reset")]
[RequireScope("manage")]
public async Task<IActionResult> Reset(CancellationToken ct)
{
2026-09-13 08:35:17 +00:00
if (!IsPlatformAdmin())
{
return StatusCode(StatusCodes.Status403Forbidden, new
{
error = "Resetting a tenant quota requires the platform admin key " +
"(header X-Admin-Key, configured as Workflows:AdminApiKey).",
});
}
2026-09-11 22:02:46 +00:00
return Ok(await _quota.ResetAsync(TenantId, ct));
}
2026-09-13 08:35:17 +00:00
private bool IsPlatformAdmin()
{
if (_adminKey == null)
return false;
var provided = Request.Headers["X-Admin-Key"].FirstOrDefault();
return provided != null && FixedTimeEquals(provided, _adminKey);
}
private static bool FixedTimeEquals(string a, string b)
{
var aBytes = System.Text.Encoding.UTF8.GetBytes(a);
var bBytes = System.Text.Encoding.UTF8.GetBytes(b);
if (aBytes.Length != bBytes.Length)
return false;
var result = 0;
for (var i = 0; i < aBytes.Length; i++)
result |= aBytes[i] ^ bBytes[i];
return result == 0;
}
2026-09-11 22:02:46 +00:00
}