Portal Community

Example 1 — Finance-Only Payroll Access

Scenario: Only permanent Finance employees can initiate payroll workflows. Contractors and users from other departments are denied even if they have the manager role.

public sealed class FinancePayrollAccessResolver(
    IUserProfileService profiles, IMemoryCache cache) : ICustomPermissionResolver
{
    public async Task<PermissionResolution> ResolveAsync(PermissionContext ctx, CancellationToken ct)
    {
        if (ctx.ResourceType != "payroll") return PermissionResolution.Defer;

        var profile = await cache.GetOrCreateAsync($"profile:{ctx.User.UserId}", async e => {
            e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2);
            return await profiles.GetAsync(ctx.User.UserId, ct);
        });

        if (profile is null)                          return PermissionResolution.Deny;
        if (profile.EmployeeType == "contractor")     return PermissionResolution.Deny;
        if (profile.Department    != "Finance")       return PermissionResolution.Deny;

        return PermissionResolution.Allow;
    }
}

Example 2 — Time-of-Day Restrictions

Scenario: Live payment execution is only allowed during business hours (09:00-17:00 UTC, Monday-Friday). Scheduled (non-real-time) payments are unrestricted.

public sealed class LivePaymentHoursResolver : ICustomPermissionResolver
{
    private static readonly HashSet<DayOfWeek> Weekdays =
    [
        DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday,
        DayOfWeek.Thursday, DayOfWeek.Friday
    ];

    public Task<PermissionResolution> ResolveAsync(PermissionContext ctx, CancellationToken ct)
    {
        // Only applies to live payment execution
        if (ctx.ResourceType != "live-payment" || ctx.Permission != "workflow.initiate")
            return Task.FromResult(PermissionResolution.Defer);

        var now = ctx.Environment.Now;
        bool isWeekday    = Weekdays.Contains(now.DayOfWeek);
        bool isBusinessHr = now.Hour >= 9 && now.Hour < 17;

        return Task.FromResult(
            isWeekday && isBusinessHr
                ? PermissionResolution.Defer   // business hours → let RBAC decide
                : PermissionResolution.Deny);  // outside hours → deny always
    }
}

Example 3 — Geographic ABAC (GDPR Data Residency)

Scenario: EU-regulated data can only be accessed from EU countries. Access from outside the EU is denied regardless of role.

public sealed class EuDataResidencyResolver(
    IGeoIpService geoIp) : ICustomPermissionResolver
{
    private static readonly HashSet<string> EuCountries =
        ["DE", "FR", "NL", "BE", "AT", "SE", "DK", "FI", "IT", "ES",
         "PL", "PT", "IE", "LU", "HR", "BG", "RO", "HU", "SK", "SI"];

    public async Task<PermissionResolution> ResolveAsync(PermissionContext ctx, CancellationToken ct)
    {
        if (ctx.Resource?.Classification != "EU_REGULATED")
            return PermissionResolution.Defer;

        var ip      = ctx.Environment.RequestIp;
        var country = ip is not null
            ? await geoIp.GetCountryAsync(ip, ct)
            : null;

        return country is not null && EuCountries.Contains(country)
            ? PermissionResolution.Defer    // EU country → let RBAC decide
            : PermissionResolution.Deny;    // non-EU or unknown → deny
    }
}

Example 4 — Project-Based Dynamic Roles

Scenario: Users get the project-lead role only when they have an active project assignment as lead. This role grants design access to project-specific workflow templates.

public sealed class ProjectLeadRoleProvider(
    IProjectRepository projects, IMemoryCache cache) : ICustomRoleProvider
{
    public async Task<IReadOnlyList<string>> GetRolesAsync(
        string userId, string tenantId, CancellationToken ct)
    {
        var key = $"project-lead:{userId}:{tenantId}";

        return await cache.GetOrCreateAsync(key, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(3);

            bool isLead = await projects.HasActiveLeadRoleAsync(userId, tenantId, ct);
            return isLead
                ? (IReadOnlyList<string>)["project-lead"]
                : Array.Empty<string>();
        }) ?? [];
    }
}
Checklist Before Deploying Custom IAM
  • All resolvers return Defer (not Deny) for resources/permissions they don't apply to
  • External calls are cached with appropriate TTLs
  • Resolvers catch and log exceptions, returning Defer (not throwing)
  • Unit tests cover Allow, Deny, and Defer scenarios
  • Integration test confirms end-to-end behavior in the full pipeline
  • Audit logging is in place for every Deny decision with reason
  • Performance tested under load (resolver should not add >10ms to p99)