Portal Community

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

1

Identity Resolution

Extract UserId, TenantId, and base claims from the ClaimsPrincipal using IIdentityProvider. If any required claim is missing: DENY immediately.

2

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.

3

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.

4

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.

5

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.

6

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 LevelTTLKey
Membership (roles)5 min (token-based)UserId + TenantId
Permission sets10 min (in-memory)RoleId
Resource policies2 min (distributed)ResourceType + ResourceId
Policy decisionsNot 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
}