Policy Evaluation
How the IAM engine evaluates a permission check — the six-step pipeline from identity resolution to final allow/deny decision, with caching and audit logging at each stage.
IIAMPolicyEngine
namespace BizFirst.Ai.ProcessSecurity.Extended.Domain;
public interface IIAMPolicyEngine
{
/// <summary>
/// Evaluate whether a subject can perform an action on a resource.
/// Subject: token claims principal. Resource: optional resource identifier.
/// Returns Allow or Deny — never throws on evaluation failure (returns Deny).
/// </summary>
Task<PolicyDecision> EvaluateAsync(
PolicyEvaluationRequest request,
CancellationToken ct = default);
}
public record PolicyEvaluationRequest
{
public required ClaimsPrincipal Subject { get; init; }
public required string Permission { get; init; } // e.g., "workflow.execute"
public string? ResourceId { get; init; } // optional resource scoping
public string? ResourceType { get; init; } // e.g., "workflow", "form"
public IReadOnlyDictionary<string, object> Context { get; init; } = new Dictionary<string, object>();
}
public record PolicyDecision
{
public required bool Allow { get; init; }
public string? Reason { get; init; } // why the decision was made
public string? DecisionSource { get; init; } // which layer made the decision
}
Evaluation Pipeline
Identity Resolution
Extract UserId, TenantId, and base claims from the ClaimsPrincipal using IIdentityProvider. If any required claim is missing: DENY immediately.
Membership Resolution
IMembershipProvider.GetUserGroupsAsync() returns the user's current role IDs. These are resolved from the JWT claim or the identity provider's API. If this call fails, the cached result is used; if no cache exists: DENY.
Permission Expansion
For each role, look up the role's permission set. Expand wildcard permissions (workflow.*). Build the effective permission set as the union of all role permissions plus explicit user permissions from IPermissionProvider.
Permission Match
Check if the requested permission is in the effective set. Exact match first; then wildcard match (workflow.* covers workflow.design). If matched: tentative ALLOW.
Resource Policy Check
If a ResourceId is provided, look up resource-level policies. A resource-level DENY overrides the tentative ALLOW. A resource-level ALLOW on an otherwise denied permission still requires the base role permission — resource policies cannot grant new permissions, only restrict existing ones.
Access Decision Provider
IAccessDecisionProvider.CanUserAccessAsync(userId, nodeId) — optional final gate. A false response forces DENY even if previous steps returned ALLOW. A null response means "no opinion" — don't change the current decision.
Deny-by-Default Principle
The policy engine is deny-by-default. A permission check returns ALLOW only if every evaluation step either returns ALLOW or abstains. Any step returning DENY is final.
// Evaluation outcomes summary
Step 1 — Missing claims → DENY (immediately, no further evaluation)
Step 2 — Membership failure → DENY (no roles = no permissions)
Step 3 — Permission not in set → DENY (no role grants this permission)
Step 4 — Wildcard match found → tentative ALLOW
Step 5 — Resource DENY policy → DENY (overrides previous ALLOW)
Step 6 — AccessDecision false → DENY (final override)
Step 6 — AccessDecision null → previous decision stands
Caching Strategy
| Cache Level | TTL | Key |
|---|---|---|
| Membership (roles) | 5 min (token-based) | UserId + TenantId |
| Permission sets | 10 min (in-memory) | RoleId |
| Resource policies | 2 min (distributed) | ResourceType + ResourceId |
| Policy decisions | Not cached | — (always fresh) |
Audit Logging
Every non-trivial policy evaluation is written to the security audit log. This includes the decision, the reason, and the context:
// Audit log entry for a policy evaluation
{
"eventType": "PolicyEvaluated",
"timestamp": "2026-05-25T10:00:00Z",
"userId": "user-guid",
"tenantId": "tenant-abc",
"permission": "workflow.execute",
"resourceId": "workflow-guid",
"decision": "Allow",
"decisionSource": "RolePermission",
"reason": "Role 'manager' grants 'workflow.execute'",
"rolesEvaluated": ["manager", "finance-user"],
"durationMs": 12
}