Skip to content

Cyrus-0101/UlinziLib

Repository files navigation

UlinziLib

A self-contained .NET 8 security library providing ASP.NET Core Identity, multi-provider authentication, session management, MFA, audit logging, password/lockout policies, and role-based permissions — all isolated in a dedicated PostgreSQL security schema.


Table of Contents

  1. What UlinziLib Provides
  2. Architecture Overview
  3. Quick Start (ProjectReference)
  4. Quick Start (NuGet)
  5. Running Migrations
  6. Configuration Reference
  7. IP Whitelist Opt-In
  8. Permission Key Reference
  9. Role Reference
  10. Extending UlinziLib
  11. Post-Publishing Workflow
  12. Repository Strategy
  13. Testing

What UlinziLib Provides

Domain What is included
Identity User, Role, Permission, Team entities extending ASP.NET Core Identity
Authentication IUserRepository with multi-provider fields (Local, Zitadel, Entra, Google)
Session Management IHttpSessionRecordRepository, ISessionPolicyService — session creation, expiry, idle timeout
MFA IMfaService, AppUserMfa — TOTP (via Otp.NET), recovery codes
Audit IAuditService, SecurityAuditEvent — tamper-evident event log
Password Policy IPasswordPolicyService, PasswordPolicy — min/max length, complexity, history, breach check
Lockout Policy ILockoutPolicyService, LockoutPolicy — progressive lockout, admin unlock, notifications
Login Attempts ILoginAttemptsService, LoginAttempt — per-IP and per-email tracking
Account Lockouts IAccountLockoutsService, AccountLockout — manual and automated lockout records
Authorization IAuthorizationService — check permission.key against a user's role-resolved permissions
Invite Tracking IInviteEmailLogRepository, InviteEmailLog — email invite delivery lifecycle
Seeded Data 7 migrations — 40+ permissions, 13+ roles, initial admin user, all idempotent

Everything lives in the security PostgreSQL schema. No collision with your application schema.


Architecture Overview

┌───────────────────────────────────────────────────────┐
│                    Host Application                    │
│                                                        │
│  Program.cs                                            │
│  ├── services.AddUlinziLib(connStr)                  │
│  └── services.AddUlinziLibIpWhitelist()  [optional]  │
│                                                        │
│  Middleware Pipeline                                   │
│  ├── IpWhitelistMiddleware          [if enabled]       │
│  ├── SessionEnforcementMiddleware                      │
│  ├── UseAuthentication()                               │
│  ├── CsrfProtectionMiddleware                          │
│  ├── UseAuthorization()                                │
│  └── SessionWriteGuardMiddleware                       │
│                                                        │
│  Authentication/ (host layer)                          │
│  └── AuthSessionService — multi-provider login         │
│      Local → Zitadel → Entra → permission resolution  │
└───────────────┬───────────────────────────────────────┘
                │ ProjectReference / NuGet
┌───────────────▼───────────────────────────────────────┐
│                  UlinziLib                           │
│                                                        │
│  Domain/Entities/    ← User, Role, Permission, Team,   │
│                         SessionPolicy, AppUserMfa,     │
│                         SecurityAuditEvent, etc.       │
│                                                        │
│  Application/        ← Interfaces (IUserRepository,   │
│                         IMfaService, IAuditService…)   │
│                                                        │
│  Infrastructure/     ← Implementations + Repos        │
│  │  Data/            ← SecurityDbContext               │
│  └  Services/        ← PasswordPolicyService, etc.     │
│                                                        │
│  Extensions/         ← AddUlinziLib()               │
│  Migrations/         ← 7 migrations (security schema)  │
└───────────────────────────────────────────────────────┘

Quick Start (ProjectReference)

This is the current setup while UlinziLib lives in the same solution.

1. Add the project reference

In your host .csproj:

<ItemGroup>
  <ProjectReference Include="..\UlinziLib\UlinziLib.csproj" />
</ItemGroup>

2. Register in Program.cs

using UlinziLib.Extensions;

var defaultConnection = builder.Configuration.GetConnectionString("DefaultConnection");

builder.Services.AddUlinziLib(defaultConnection!);

3. Apply migrations at startup

// In Program.cs, after app is built:
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<SecurityDbContext>();
    await db.Database.MigrateAsync();
}

4. Register middleware (order matters)

// Optional - must be FIRST if used
app.UseMiddleware<IpWhitelistMiddleware>();

// Required session enforcement
app.UseMiddleware<SessionEnforcementMiddleware>();
app.UseAuthentication();
app.UseMiddleware<CsrfProtectionMiddleware>();
app.UseAuthorization();
app.UseMiddleware<SessionWriteGuardMiddleware>();

Quick Start (NuGet)

Once UlinziLib is published as UlinziLib:

dotnet add package UlinziLib

The setup is identical to the ProjectReference approach above. The key difference is migrations:

// Tell EF Core where the migration assembly lives
builder.Services.AddDbContext<SecurityDbContext>(options =>
{
    options.UseNpgsql(connStr, npgsql =>
    {
        npgsql.MigrationsHistoryTable("__EFMigrationsHistory", "security");
        npgsql.MigrationsAssembly("UlinziLib"); // ← assembly name of the package
    });
});

Then db.Database.MigrateAsync() at startup picks up all bundled migrations.


Running Migrations

Automatic (recommended for staging/production)

Call MigrateAsync() in Program.cs as shown above. All 7 migrations run once and are recorded in security.__EFMigrationsHistory. They are idempotent — re-running is safe.

CLI (development)

Run from the repository root (the directory containing your host app's .csproj). Always pass --context SecurityDbContext because the startup project may contain multiple DbContexts.

# List all migrations and see which are applied (marked with [X])
dotnet ef migrations list \
  --project UlinziLib/UlinziLib.csproj \
  --startup-project YourApp.csproj \
  --context SecurityDbContext \
  --no-build

# Apply all pending migrations
dotnet ef database update \
  --project UlinziLib/UlinziLib.csproj \
  --startup-project YourApp.csproj \
  --context SecurityDbContext \
  --no-build

# Create a new migration inside UlinziLib
dotnet ef migrations add MyNewMigration \
  --project UlinziLib/UlinziLib.csproj \
  --startup-project YourApp.csproj \
  --context SecurityDbContext

# Roll back to a specific migration
dotnet ef database update PreviousMigrationName \
  --project UlinziLib/UlinziLib.csproj \
  --startup-project YourApp.csproj \
  --context SecurityDbContext \
  --no-build

NuGet NU1301 workaround: If dotnet ef fails with NU1301 signature-verification errors, run a normal build first (which restores packages successfully), then use --no-build to skip the restore step on subsequent dotnet ef calls:

dotnet build YourApp.csproj   # restore + build once
dotnet ef database update ... --no-build

Design-Time Factory

UlinziLib includes (or should include) an IDesignTimeDbContextFactory<SecurityDbContext> so dotnet ef can resolve the context without the host running:

// UlinziLib/Infrastructure/Data/SecurityDbContextFactory.cs
public class SecurityDbContextFactory : IDesignTimeDbContextFactory<SecurityDbContext>
{
    public SecurityDbContext CreateDbContext(string[] args)
    {
        var opts = new DbContextOptionsBuilder<SecurityDbContext>()
            .UseNpgsql("Host=localhost;Database=your_app_dev;Username=postgres;Password=postgres",
                npgsql => npgsql.MigrationsHistoryTable("__EFMigrationsHistory", "security"))
            .Options;
        return new SecurityDbContext(opts);
    }
}

Important for NuGet consumers: When the package is consumed, the host application does not need to run dotnet ef migrations directly. The migrations ship inside the DLL. The host simply calls MigrateAsync() and EF discovers the bundled migrations via the assembly reference.


Configuration Reference

AddUlinziLib takes a plain connection string. All tunable behavior lives in database-backed policy tables (not appsettings.json) so changes take effect without redeployment.

Table Configures
security.session_policies SessionTimeoutMinutes (1–10080), IdleTimeoutMinutes (1–1440), MaxSessionsPerUser (1–50)
security.password_policies Min/max length, complexity rules, history count, max age, breach DB check
security.lockout_policies Max failed attempts, lockout duration, progressive multiplier, permanent threshold

These are read at runtime by ISessionPolicyService, IPasswordPolicyService, and ILockoutPolicyService respectively.


IP Whitelist Opt-In

IP Whitelisting is a planned UlinziLib feature. When implemented:

Registration

// After AddUlinziLib()
builder.Services.AddUlinziLibIpWhitelist(options =>
{
    options.DefaultMode = IpWhitelistMode.Enforce; // or Monitor
    options.CacheTtlSeconds = 30;
    options.ExemptPaths = ["/health", "/metrics"];
});

Middleware (register BEFORE session enforcement)

app.UseMiddleware<IpWhitelistMiddleware>();       // ← first
app.UseMiddleware<SessionEnforcementMiddleware>();

Permission Required

The permission settings.canManageIPWhitelisting is already seeded and assigned to the Security Admin role. No additional migration is needed.

Enforcement Modes

Mode Behavior
Enforce Block requests from non-whitelisted IPs (return 403)
Monitor Allow all requests but log violations to security.ip_access_logs

Permission Key Reference

All permission keys are seeded in migrations and can be queried with:

SELECT name, display_name, category FROM security.permissions ORDER BY category, name;

Core Permission Keys

Key Category Description
users.canManage Users Create/edit/disable users
users.create Users Create new users
users.read Users View user details
users.update Users Edit user information
users.delete Users Delete users
roles.canManage Roles Create/edit/delete roles
roles.canAssign Roles Assign roles to users
teams.canManage Teams Create/edit/delete teams
teams.canAssign Teams Assign users to teams
teams.read Teams View teams
impersonation.canImpersonate Users View-as another user
settings.read Settings View system settings
settings.update Settings Modify system settings
settings.security Settings Manage security settings
settings.canManageAuthentication Settings Configure SSO/password settings
settings.canManageEmailSetup Settings Configure email providers
settings.canManageSMSSetup Settings Configure SMS providers
settings.canManageWebhooks Settings Configure webhook endpoints
settings.canManageIPWhitelisting Settings Configure IP restrictions
settings.canManageUtilities Settings Access developer tools
settings.canManageAISetting Settings Manage AI settings
settings.canManageATSSetup Settings Manage ATS configuration
settings.canManageGlobalLimits Settings Manage global usage limits
logs.canView Logs Access activity logs
analytics.viewOwnCampaigns Analytics View own campaign stats
analytics.viewTeamCampaigns Analytics View team campaigns
analytics.viewSameTeamData Analytics Campaigns, lists, and activity
analytics.viewInactiveData Analytics Data from disabled accounts
analytics.viewRecipientData Analytics Individual delivery details
communication.email Communication Send email campaigns
communication.sms Communication Send SMS campaigns
communication.customFromAddress Communication Use custom From addresses
communication.customReplyTo Communication Use team email in reply-to
contentTemplates.customHtml Content Create custom HTML templates
contentTemplates.exemptFromFooter Content Skip mandatory footer in campaigns
lists.canImport Lists Import recipients from files
campaigns.read Campaigns View campaigns
contacts.read Contacts View contacts
reports.view Reports View reports
api.canAccess API Access Access and use the API

Checking Permissions in Code

// Inject IAuthorizationService (UlinziLib's, not ASP.NET Core's)
var allowed = await _authorizationService.HasPermissionAsync(userId, "settings.canManageIPWhitelisting");

Role Reference

Seeded roles and their intent:

Role System Role Default Assigned Permissions
SuperAdmin Yes No All permissions
Admin Yes No All except users.delete, settings.security
User Yes Yes Basic read access
System Yes No All permissions (machine-to-machine)
Full Admin No No All permissions
Comms Admin No No Communication + Email/SMS settings
Config Admin No No Settings category
HR Admin No No Users, Roles, Teams + impersonation
Reports Admin No No Analytics, Logs, Reports
Security Admin No No Auth, IP Whitelisting, impersonation, logs
Analyst No No Analytics + Reports
Coordinator No No Lists + Communication
Outreach Rep No No Communication + Lists + Analytics
Recruiter No No Communication + Lists + Analytics
Senior Recruiter No No Communication + Lists + Analytics + custom HTML
Sourcer No No Communication + Lists + Analytics
Team Lead No No Communication + Lists + Analytics
Manager No No Communication + Lists + Analytics + team data

Extending UlinziLib

Adding a New Entity

  1. Create the entity in UlinziLib/Domain/Entities/:
// Domain/Entities/MyNewEntity.cs
namespace UlinziLib.Domain.Entities;

public class MyNewEntity
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public DateTime CreatedAtUtc { get; set; }
}
  1. Add a DbSet to SecurityDbContext:
public DbSet<MyNewEntity> MyNewEntities { get; set; }
  1. Configure the mapping in OnModelCreating:
modelBuilder.Entity<MyNewEntity>(e =>
{
    e.ToTable("my_new_entities", schema);
    e.HasKey(x => x.Id);
    e.Property(x => x.Id).HasColumnName("id");
    e.Property(x => x.Name).HasColumnName("name").HasMaxLength(200).IsRequired();
    e.Property(x => x.CreatedAtUtc).HasColumnName("created_at_utc").IsRequired();
});
  1. Create a migration (from host project directory):
dotnet ef migrations add AddMyNewEntity --project ../UlinziLib --startup-project .
  1. Add the interface in Application/:
// Application/IMyNewEntityRepository.cs
public interface IMyNewEntityRepository
{
    Task<MyNewEntity?> GetByIdAsync(Guid id);
    Task AddAsync(MyNewEntity entity);
}
  1. Implement it in Infrastructure/Repositories/ and register in ServiceCollectionExtensions.cs:
services.AddScoped<IMyNewEntityRepository, MyNewEntityRepository>();

Adding a New Permission

Add an idempotent INSERT ... ON CONFLICT DO NOTHING in a new migration:

migrationBuilder.Sql(@"
    INSERT INTO security.permissions (name, display_name, description, category, resource_type, action, created_at_utc)
    VALUES ('myFeature.canDoThing', 'Do Thing', 'Description', 'MyCategory', 'resource', 'action', (NOW() AT TIME ZONE 'UTC'))
    ON CONFLICT (name) DO NOTHING;
");

Then assign to the appropriate roles in the same migration using the CROSS JOIN security.permissions WHERE normalized_name = '...' pattern already established.

Adding a New Service

Follow the existing pattern:

  1. Define interface in Application/IMyService.cs
  2. Implement in Infrastructure/Services/MyService.cs
  3. Register in Extensions/ServiceCollectionExtensions.cs
  4. Add XML doc comment to the interface (required for NuGet documentation)

Post-Publishing Workflow

Step 1 — Add NuGet Metadata to the .csproj

<PropertyGroup>
  <PackageId>UlinziLib</PackageId>
  <Version>1.0.0</Version>
  <Authors>UlinziLib Contributors</Authors>
  <Description>Security library — identity, sessions, MFA, audit, permissions for .NET 8 + PostgreSQL.</Description>
  <PackageTags>security;identity;authentication;efcore;postgresql;aspnetcore</PackageTags>
  <PackageReadmeFile>README.md</PackageReadmeFile>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <RepositoryUrl>https://github.com/your-org/UlinziLib</RepositoryUrl>
  <RepositoryType>git</RepositoryType>
</PropertyGroup>

<ItemGroup>
  <None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

Step 2 — Pack the Library

dotnet pack UlinziLib/UlinziLib.csproj -c Release -o ./nupkg

This produces nupkg/UlinziLib.1.0.0.nupkg.

Step 3 — Publish

GitHub Packages (recommended for internal use):

dotnet nuget push ./nupkg/UlinziLib.1.0.0.nupkg \
  --api-key $GITHUB_TOKEN \
  --source "https://nuget.pkg.github.com/your-org/index.json"

NuGet.org (for public release):

dotnet nuget push ./nupkg/UlinziLib.1.0.0.nupkg \
  --api-key $NUGET_API_KEY \
  --source https://api.nuget.org/v3/index.json

Step 4 — Consume in the Host

dotnet add package UlinziLib --version 1.0.0

Remove the <ProjectReference> and replace with the <PackageReference> above. The AddUlinziLib() call in Program.cs is unchanged.

Step 5 — Migrations as a Consumer

When consumed as a NuGet package you cannot run dotnet ef migrations add against it (it's a binary). Instead:

Scenario How to handle
New migration needed Bump the package version in UlinziLib source, add migration, republish
Consumer applies migrations db.Database.MigrateAsync() at startup (unchanged)
Consumer wants to inspect SQL dotnet ef migrations script from the UlinziLib source project
Emergency hotfix Hotfix in UlinziLib source → patch version bump → republish → consumer bumps package ref

Repository Strategy

Option A — Monorepo (current / recommended for early stage)

Keep UlinziLib as a folder inside your host solution. Use <ProjectReference> during active development. Publish a NuGet package from CI when the main branch is tagged.

Pros: Single PR covers UlinziLib + consumer changes. No version pinning friction. Cons: UlinziLib version history is mixed into the app repo.

Option B — Standalone Repository

Move UlinziLib into its own Git repository (your-org/UlinziLib). Consume via NuGet only.

# One-time migration
git subtree split --prefix=UlinziLib -b ulinzilib-standalone
cd /tmp && git clone <your-repo> ulinzilib
cd ulinzilib && git checkout ulinzilib-standalone
git remote add origin git@github.com:your-org/UlinziLib.git
git push -u origin main

There is no dotnet new template specifically for this. The above git subtree split is the standard .NET approach for extracting a folder into its own repo with preserved history.

Pros: Clean separation. UlinziLib can be versioned and consumed by multiple apps. Cons: Two PRs for cross-cutting changes. Must manage NuGet version bumps.

Recommendation

Stay with Option A (monorepo + ProjectReference) until UlinziLib stabilises with IP Whitelisting complete and at least one additional consumer app. Then migrate to Option B with a 1.0.0 stable release.


Testing

Unit Tests

Create UlinziLib.Tests/ alongside UlinziLib/:

dotnet new xunit -n UlinziLib.Tests
dotnet add UlinziLib.Tests/UlinziLib.Tests.csproj reference UlinziLib/UlinziLib.csproj
dotnet add UlinziLib.Tests package NSubstitute
dotnet add UlinziLib.Tests package FluentAssertions

Test services by mocking the SecurityDbContext or repositories:

public class PasswordPolicyServiceTests
{
    private readonly IPasswordPolicyService _sut;
    private readonly IDbContextFactory<SecurityDbContext> _factory = Substitute.For<...>();

    [Fact]
    public async Task ValidatePassword_WhenTooShort_ReturnsFalse() { ... }
}

Integration Tests

Use Testcontainers to spin up a real PostgreSQL instance:

dotnet add UlinziLib.Tests package Testcontainers.PostgreSql
public class MigrationTests : IAsyncLifetime
{
    private readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder().Build();

    public async Task InitializeAsync()
    {
        await _postgres.StartAsync();
        // Run migrations against real Postgres
        var db = BuildDbContext(_postgres.GetConnectionString());
        await db.Database.MigrateAsync();
    }

    [Fact]
    public async Task AllPermissions_AreSeeded()
    {
        var db = BuildDbContext(_postgres.GetConnectionString());
        var count = await db.Permissions.CountAsync();
        Assert.True(count >= 40);
    }
}

Running Tests

dotnet test UlinziLib.Tests/ --logger "console;verbosity=detailed"

About

A self-contained .NET 8 security library providing ASP.NET Core Identity, multi-provider authentication, session management, MFA, audit logging, password/lockout policies, and role-based permissions - all isolated in a dedicated PostgreSQL security schema.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages