Portal Community

The Layering Model

Custom extensions do not replace the built-in IAM — they extend it. The built-in system runs first, producing a tentative ALLOW or DENY. Custom resolvers then have the opportunity to override or affirm that decision.

// Full evaluation pipeline with custom extensions
Phase 1 — Built-in:
  IIdentityProvider.ResolveIdentityAsync()   → IdentityClaims
  IMembershipProvider.GetUserGroupsAsync()   → built-in roles
  ICustomRoleProvider[0].GetRolesAsync()     → extra roles (merged)
  ICustomRoleProvider[1].GetRolesAsync()     → extra roles (merged)
  IPermissionProvider.GetUserPermissionsAsync() → permission strings
  PermissionEngine.Match()                   → BaseAllowed = true/false

Phase 2 — Custom Resolvers (pipeline, short-circuits on Allow/Deny):
  ICustomPermissionResolver[0].ResolveAsync()  → Allow/Deny/Defer
  ICustomPermissionResolver[1].ResolveAsync()  → Allow/Deny/Defer  (if previous Defer)
  ICustomPermissionResolver[2].ResolveAsync()  → Allow/Deny/Defer  (if previous Defer)

Phase 3 — Final Gate:
  IAccessDecisionProvider.CanUserAccessAsync() → true/false/null

Using context.BaseAllowed

The PermissionContext.BaseAllowed property tells you whether the built-in RBAC check produced an ALLOW. Use this to decide whether your resolver needs to act:

// Pattern 1: Only add ABAC constraints when RBAC already allows
// "RBAC is necessary but not sufficient"
public Task<PermissionResolution> ResolveAsync(PermissionContext ctx, CancellationToken ct)
{
    // If RBAC already denied, don't override — it's already denied
    if (!ctx.BaseAllowed) return Task.FromResult(PermissionResolution.Defer);

    // RBAC allowed, but add additional constraints
    if (!MeetsAbacCondition(ctx))
        return Task.FromResult(PermissionResolution.Deny);

    return Task.FromResult(PermissionResolution.Defer);  // RBAC allow stands
}

// Pattern 2: Override RBAC entirely (use sparingly)
// "Custom resolver has full authority regardless of RBAC"
public Task<PermissionResolution> ResolveAsync(PermissionContext ctx, CancellationToken ct)
{
    if (IsSystemAdmin(ctx.User))
        return Task.FromResult(PermissionResolution.Allow);  // bypass RBAC

    if (IsContractor(ctx.User) && IsSensitiveResource(ctx))
        return Task.FromResult(PermissionResolution.Deny);   // override RBAC allow

    return Task.FromResult(PermissionResolution.Defer);  // let RBAC stand
}

Interaction Matrix

RBAC ResultResolver ReturnsFinal Decision
AllowDeferAllow (RBAC result stands)
AllowAllowAllow (explicit confirmation)
AllowDenyDeny (resolver overrides RBAC)
DenyDeferDeny (RBAC result stands)
DenyAllowAllow (resolver overrides RBAC — use carefully)
DenyDenyDeny (explicit confirmation)
Overriding RBAC Denials Requires Justification

A resolver that returns Allow when BaseAllowed is false effectively bypasses the role system. This is a powerful capability — use it only for scenarios like "break-glass" emergency access or resource ownership grants. Every such override should be logged with a clear reason. Audit this code path carefully.

Multiple Resolvers — Priority and Order

// Registration order determines evaluation order
services.AddCustomPermissionResolver<ContractorRestrictionResolver>();  // first
services.AddCustomPermissionResolver<BusinessHoursResolver>();          // second
services.AddCustomPermissionResolver<ClearanceLevelResolver>();         // third

// Pipeline stops when any resolver returns Allow or Deny
// If ContractorRestrictionResolver returns Deny → done, Deny
// If ContractorRestrictionResolver returns Defer → BusinessHoursResolver is called
// If all return Defer → RBAC decision stands