API-Level Enforcement
ProcessAccessGuard middleware checks access before any controller action runs. All workflow API endpoints are protected — there is no way to reach the engine without passing the access check.
ProcessAccessGuard Attribute
[ApiController]
[Route("api/processes/{processId}")]
public class ProcessController : ControllerBase
{
[HttpGet("definition")]
[ProcessAccessGuard(ProcessAccessRole.Viewer)] // Any role ≥ Viewer
public async Task<IActionResult> GetDefinition(string processId)
{ ... }
[HttpPut("definition")]
[ProcessAccessGuard(ProcessAccessRole.Editor)] // Must be Editor or Owner
public async Task<IActionResult> UpdateDefinition(string processId, ...)
{ ... }
[HttpPost("access")]
[ProcessAccessGuard(ProcessAccessRole.Owner)] // Owner only
public async Task<IActionResult> GrantAccess(string processId, ...)
{ ... }
}
Guard Middleware Flow
Extract user from JWT
UserId and TenantId extracted from ClaimsPrincipal.
Call IProcessAccessChecker.CheckAsync
Checks (userId, processId, requiredRole) against the DB and IAM groups.
Pass — execute action
Request continues to controller action.
Fail — 403 or 404
If process exists but access denied: 403 Forbidden. If process doesn't exist in this tenant: 404 Not Found (prevents enumeration).
Access Check Caching
Access checks are cached in a sliding-expiry in-memory cache per (userId, processId) pair. Cache TTL defaults to 60 seconds. This prevents database queries on every API call while keeping access revocation near-instant (within 60 seconds of revocation, the cache expires).
// Cache key: "access:{tenantId}:{processId}:{userId}"
// TTL: 60 seconds (configurable in appsettings.json)
"ProcessAccess": {
"CacheTtlSeconds": 60
}