Portal Community

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

1

Extract user from JWT

UserId and TenantId extracted from ClaimsPrincipal.

2

Call IProcessAccessChecker.CheckAsync

Checks (userId, processId, requiredRole) against the DB and IAM groups.

3a

Pass — execute action

Request continues to controller action.

3b

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
}
Enforcement layer: The access guard is enforced at the HTTP controller layer. The execution engine layer does not perform access checks — it trusts that the API layer already validated the caller. Service accounts calling the engine directly must use an appropriate bearer token with access to the workflow.