Passport
Custom Permission Resolver
Implement ICustomPermissionResolver to add ABAC (Attribute-Based Access Control) rules — return Allow, Deny, or Defer based on user attributes, resource context, and environment conditions.
ICustomPermissionResolver
namespace BizFirst.Essentials.Passport.IAM;
public interface ICustomPermissionResolver
{
/// <summary>
/// Evaluate a permission check with ABAC logic.
/// Called after built-in role/permission evaluation.
/// Return:
/// Allow — explicitly grant (short-circuits pipeline, stops further resolvers)
/// Deny — explicitly deny (short-circuits pipeline, stops further resolvers)
/// Defer — no opinion, continue to next resolver
/// </summary>
Task<PermissionResolution> ResolveAsync(
PermissionContext context,
CancellationToken ct = default);
}
public class PermissionContext
{
public required IDInfo User { get; init; }
public required string Permission { get; init; } // e.g., "form.submit"
public string? ResourceId { get; init; }
public string? ResourceType{ get; init; } // e.g., "form"
public ResourceAttributes? Resource { get; init; } // resource metadata (if available)
public EnvironmentContext Environment { get; init; } = new();
public bool BaseAllowed { get; init; } // result of role-based check
}
public class ResourceAttributes
{
public string? OwnerId { get; init; }
public string? Department { get; init; }
public string? Classification { get; init; }
public IReadOnlyDictionary<string, object> Custom { get; init; } = new Dictionary<string, object>();
}
public class EnvironmentContext
{
public DateTimeOffset Now { get; init; } = DateTimeOffset.UtcNow;
public string? RequestIp { get; init; }
public string? Country { get; init; }
public bool IsMfaVerified { get; init; }
}
Simple ABAC Resolver — Department Check
/// <summary>
/// Restricts payroll-related permissions to Finance department employees only.
/// Contractors (EmployeeType == "contractor") are always denied payroll access.
/// </summary>
public sealed class DepartmentBasedPayrollResolver : ICustomPermissionResolver
{
private static readonly string[] PayrollPermissions =
["workflow.initiate", "form.submit", "report.payroll.read"];
private readonly IUserProfileService _profiles;
public DepartmentBasedPayrollResolver(IUserProfileService profiles)
=> _profiles = profiles;
public async Task<PermissionResolution> ResolveAsync(
PermissionContext context,
CancellationToken ct)
{
// Only apply to payroll resources
if (context.ResourceType != "payroll" &&
context.ResourceId?.StartsWith("payroll-") != true)
return PermissionResolution.Defer;
// Only apply to relevant permissions
if (!PayrollPermissions.Contains(context.Permission))
return PermissionResolution.Defer;
// Load user profile attributes
var profile = await _profiles.GetAsync(context.User.UserId, ct);
if (profile is null) return PermissionResolution.Deny;
// Contractors always denied payroll access
if (profile.EmployeeType == "contractor")
return PermissionResolution.Deny;
// Finance department members allowed
if (profile.Department == "Finance")
return PermissionResolution.Allow;
// No opinion for other departments — let role check stand
return PermissionResolution.Defer;
}
}
Business Hours Resolver
public sealed class BusinessHoursPaymentResolver : ICustomPermissionResolver
{
public Task<PermissionResolution> ResolveAsync(
PermissionContext context,
CancellationToken ct)
{
// Only apply to live payment workflows
if (context.ResourceType != "payment-workflow")
return Task.FromResult(PermissionResolution.Defer);
var now = context.Environment.Now;
var dayOfWeek = now.DayOfWeek;
var hour = now.Hour;
// Allow Monday-Friday 9:00-17:00 UTC
bool isBusinessHours =
dayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday &&
hour is >= 9 and < 17;
return Task.FromResult(
isBusinessHours
? PermissionResolution.Defer // let normal role check stand
: PermissionResolution.Deny); // outside hours → deny always
}
}
Resource Ownership Resolver
/// <summary>
/// Allows users to edit their own resources even if their base role only grants "view".
/// Example: a user with "viewer" role can still edit forms they created.
/// </summary>
public sealed class ResourceOwnershipResolver : ICustomPermissionResolver
{
public Task<PermissionResolution> ResolveAsync(
PermissionContext context,
CancellationToken ct)
{
// Only apply to edit permissions
if (!context.Permission.EndsWith(".edit") && !context.Permission.EndsWith(".delete"))
return Task.FromResult(PermissionResolution.Defer);
// Check if user owns the resource
if (context.Resource?.OwnerId == context.User.UserId)
return Task.FromResult(PermissionResolution.Allow);
return Task.FromResult(PermissionResolution.Defer);
}
}
Resolver Performance
Custom resolvers are called on every permission check in the hot path. Avoid blocking I/O without caching. If your resolver calls an external service, implement an in-memory cache with a short TTL (30 seconds to 2 minutes) to prevent latency spikes. A resolver that takes >50ms will noticeably degrade API response times.