Skip to content

Permissions & Authorization

GrydCrud integrates with GrydAuth to provide automatic permission-based authorization for CRUD controllers. With GrydCrud.Auth, your controllers can enforce permissions without boilerplate overrides.

📦 Installation

bash
dotnet add package GrydCrud.Auth

How It Works

GrydCrud.Auth operates in two layers:

LayerPurposeMechanism
Seed-timeGenerate permission strings for database seedingICrudPermissionGenerator scans [AutoPermission] controllers
RuntimeEnforce permissions on CRUD endpointsCrudPermissionConvention applies AuthorizeFilter at startup

The [AutoPermission] attribute works as a single source of truth — it feeds both layers:

[AutoPermission("products")]

        ├── Seed-time → ICrudPermissionGenerator → "create:products", "read:products", ...

        └── Runtime   → CrudPermissionConvention → AuthorizeFilter("Permission:read:products")
                                                    └→ PermissionPolicyProvider resolves policy
                                                        └→ PermissionAuthorizationHandler checks claims

Setup

csharp
// Program.cs
builder.Services.AddGrydCrudAuth(autoPermissions: true);

This registers both the ICrudPermissionGenerator and the CrudPermissionConvention.

Option 2: Via MvcOptions

If you prefer explicit control over MVC configuration:

csharp
// Program.cs
builder.Services.AddGrydCrudAuth(); // Just the permission generator

builder.Services.AddControllers(options =>
{
    options.AddCrudPermissionConvention(); // Add the convention manually
});

Option 3: Seed-Only (No runtime enforcement)

If you only want permission generation for seeding, without automatic runtime enforcement:

csharp
// Program.cs
builder.Services.AddGrydCrudAuth(); // No autoPermissions flag

Automatic Permission Enforcement

With CrudPermissionConvention enabled, controllers decorated with [AutoPermission] automatically get authorization on all CRUD actions — including inherited ones from CrudController and CqrsCrudController.

Before (Manual Overrides)

Without the convention, you had to override every CRUD method just to apply [RequirePermission]:

csharp
[AutoPermission("departments")]
[Route("api/[controller]")]
public class DepartmentsController : CrudController<Department, CreateDepartmentDto, UpdateDepartmentDto, DepartmentDto, DeptQueryParams>
{
    public DepartmentsController(ICrudService<...> service) : base(service) { }

    // ❌ 25+ lines of boilerplate just for permissions
    [RequirePermission("read:departments")]
    public override Task<IActionResult> GetAll([FromQuery] DeptQueryParams parameters, CancellationToken ct)
        => base.GetAll(parameters, ct);

    [RequirePermission("read:departments")]
    public override Task<IActionResult> GetById(Guid id, CancellationToken ct)
        => base.GetById(id, ct);

    [RequirePermission("create:departments")]
    public override Task<IActionResult> Create([FromBody] CreateDepartmentDto dto, CancellationToken ct)
        => base.Create(dto, ct);

    [RequirePermission("update:departments")]
    public override Task<IActionResult> Update(Guid id, [FromBody] UpdateDepartmentDto dto, CancellationToken ct)
        => base.Update(id, dto, ct);

    [RequirePermission("delete:departments")]
    public override Task<IActionResult> Delete(Guid id, CancellationToken ct)
        => base.Delete(id, ct);
}

After (Automatic) ✅

With the convention enabled, the same controller becomes:

csharp
[AutoPermission("departments")]
[Route("api/[controller]")]
public class DepartmentsController : CrudController<Department, CreateDepartmentDto, UpdateDepartmentDto, DepartmentDto, DeptQueryParams>
{
    public DepartmentsController(ICrudService<...> service) : base(service) { }

    // ✅ That's it! Permissions are applied automatically:
    // GET    /api/v1/departments       → Permission:read:departments
    // GET    /api/v1/departments/{id}  → Permission:read:departments
    // POST   /api/v1/departments       → Permission:create:departments
    // PUT    /api/v1/departments/{id}  → Permission:update:departments
    // DELETE /api/v1/departments/{id}  → Permission:delete:departments
}

This works with both CrudController and CqrsCrudController.

Permission Mapping

The convention maps actions to permissions using this priority:

1. Known CRUD Method Names

Methods inherited from CrudController / CqrsCrudController:

MethodPermission Action
GetAllread
GetByIdread
Createcreate
Updateupdate
Deletedelete

2. HTTP Method Attributes

For custom actions, the HTTP verb determines the action:

AttributePermission Action
[HttpGet]read
[HttpPost]create
[HttpPut] / [HttpPatch]update
[HttpDelete]delete

3. Method Name Patterns

As a fallback, the method name is analyzed:

Method Name Starts WithPermission Action
Get, List, Find, Searchread
Create, Add, Insertcreate
Update, Edit, Modify, Patchupdate
Delete, Remove, Destroydelete

Customizing Permissions

Custom Action Names

Use method-level [AutoPermission] with ActionName to define custom actions:

csharp
[AutoPermission("reports")]
[Route("api/[controller]")]
public class ReportsController : CrudController<Report, ReportDto>
{
    public ReportsController(ICrudService<Report, ReportDto> service) : base(service) { }

    // Standard CRUD: read:reports, create:reports, etc. (automatic)

    // Custom action: export:reports
    [HttpGet("export")]
    [AutoPermission("reports", ActionName = "export")]
    public async Task<IActionResult> Export() { ... }
}

Custom Entity Names Per Method

Override the entity name for specific methods:

csharp
[AutoPermission("orders")]
[Route("api/[controller]")]
public class OrdersController : CrudController<Order, OrderDto>
{
    public OrdersController(ICrudService<Order, OrderDto> service) : base(service) { }

    // Standard CRUD: read:orders, create:orders, etc.

    // This method uses a different entity: read:order-items
    [HttpGet("{id}/items")]
    [AutoPermission("order-items")]
    public async Task<IActionResult> GetOrderItems(Guid id) { ... }
}

Excluding Specific Endpoints

Mark endpoints as public by excluding them from permission enforcement:

csharp
[AutoPermission("products")]
[Route("api/[controller]")]
public class ProductsController : CrudController<Product, ProductDto>
{
    public ProductsController(ICrudService<Product, ProductDto> service) : base(service) { }

    // Standard CRUD endpoints get permissions automatically

    // This endpoint is public — no permission required
    [HttpGet("featured")]
    [AutoPermission("products", IncludeInPermissions = false)]
    [AllowAnonymous]
    public async Task<IActionResult> GetFeatured() { ... }
}

Explicit Override Takes Precedence

If a method already has [Authorize] or [RequirePermission], the convention does not override it:

csharp
[AutoPermission("documents")]
[Route("api/[controller]")]
public class DocumentsController : CrudController<Document, DocumentDto>
{
    public DocumentsController(ICrudService<Document, DocumentDto> service) : base(service) { }

    // This method has an explicit policy — convention skips it
    [Authorize(Policy = "DocumentOwnerOrAdmin")]
    public override Task<IActionResult> Delete(Guid id, CancellationToken ct)
        => base.Delete(id, ct);

    // Other CRUD methods still get automatic permissions:
    // GetAll   → Permission:read:documents
    // GetById  → Permission:read:documents
    // Create   → Permission:create:documents
    // Update   → Permission:update:documents
}

Permission Generation for Seeding

The ICrudPermissionGenerator scans assemblies for controllers with [AutoPermission] and generates permission strings for database seeding:

csharp
using GrydCrud.Auth.Abstractions;

public class PermissionSeeder
{
    private readonly ICrudPermissionGenerator _generator;
    private readonly IPermissionRepository _permissionRepository;

    public async Task SeedPermissionsAsync()
    {
        // Scan all controllers for [AutoPermission] attributes
        var permissions = _generator.GeneratePermissions(
            typeof(ProductsController).Assembly,
            typeof(OrdersController).Assembly
        );

        // Result: ["create:products", "read:products", "update:products", "delete:products",
        //          "create:orders", "read:orders", ...]

        foreach (var permission in permissions)
        {
            await _permissionRepository.CreateIfNotExistsAsync(permission);
        }
    }
}

You can also generate standard CRUD permissions without scanning assemblies:

csharp
var permissions = _generator.GenerateStandardCrudPermissions("invoices");
// Result: ["create:invoices", "read:invoices", "update:invoices", "delete:invoices"]

CrudActions Helper

Use CrudActions constants for type-safe permission strings:

csharp
using GrydCrud.Auth;

// Constants
CrudActions.Create  // "create"
CrudActions.Read    // "read"
CrudActions.Update  // "update"
CrudActions.Delete  // "delete"

// Helper methods
CrudActions.CreatePermission("products")  // "create:products"
CrudActions.ReadPermission("products")    // "read:products"
CrudActions.UpdatePermission("products")  // "update:products"
CrudActions.DeletePermission("products")  // "delete:products"
CrudActions.Permission("export", "reports")  // "export:reports"

Complete Example

Here's a full example combining all features:

csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add GrydAuth (authentication + authorization)
builder.Services.AddGrydAuth(builder.Configuration);

// Add GrydCrud with auth integration (enables convention)
builder.Services.AddGrydCrud(options => { options.UseSoftDelete = true; });
builder.Services.AddGrydCrudAuth(autoPermissions: true);

builder.Services.AddControllers();

var app = builder.Build();

app.UseAuthentication();
app.UseGrydAuth();
app.UseAuthorization();

app.MapControllers();
app.Run();
csharp
// Controllers/ProductsController.cs
using Gryd.API.Attributes;
using GrydCrud.API.Controllers;
using GrydCrud.Services.Abstractions;

[AutoPermission("products")]
[Route("api/[controller]")]
public class ProductsController : CrudController<Product, CreateProductDto, UpdateProductDto, ProductDto, ProductQueryParams>
{
    public ProductsController(
        ICrudService<Product, CreateProductDto, UpdateProductDto, ProductDto, ProductQueryParams> service)
        : base(service)
    {
    }

    // All 5 CRUD endpoints are protected automatically:
    // GET    /api/v1/products       → read:products
    // GET    /api/v1/products/{id}  → read:products
    // POST   /api/v1/products       → create:products
    // PUT    /api/v1/products/{id}  → update:products
    // DELETE /api/v1/products/{id}  → delete:products

    // Custom endpoint with custom permission
    [HttpGet("export")]
    [AutoPermission("products", ActionName = "export")]
    public async Task<IActionResult> ExportProducts(CancellationToken ct)
    {
        // export:products permission required
        return Ok();
    }

    // Public endpoint — no auth required
    [HttpGet("featured")]
    [AutoPermission("products", IncludeInPermissions = false)]
    [AllowAnonymous]
    public async Task<IActionResult> GetFeatured(CancellationToken ct)
    {
        return Ok();
    }
}

Architecture

The permission system consists of the following components:

┌──────────────────────────────────────────────────────────────────────┐
│                        Application Startup                          │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │ CrudPermissionConvention (IControllerModelConvention)          │  │
│  │  → Scans controllers with [AutoPermission]                    │  │
│  │  → Adds AuthorizeFilter("Permission:{action}:{entity}")      │  │
│  │  → Maps HTTP verbs / method names → CRUD actions              │  │
│  │  → Skips methods with existing [Authorize] / [RequirePermission] │
│  └────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────┘

                                 ▼ (at request time)
┌──────────────────────────────────────────────────────────────────────┐
│                    ASP.NET Core Authorization Pipeline               │
│                                                                      │
│  AuthorizeFilter(policy: "Permission:read:products")                 │
│         │                                                            │
│         ▼                                                            │
│  PermissionPolicyProvider                                            │
│    → Resolves "Permission:read:products" to AuthorizationPolicy      │
│    → Adds PermissionRequirement("read:products")                     │
│         │                                                            │
│         ▼                                                            │
│  PermissionAuthorizationHandler                                      │
│    → Checks User.HasClaim("permission", "read:products")            │
│    → Admin role bypasses all checks                                  │
└──────────────────────────────────────────────────────────────────────┘

                                 ▼ (at seed time)
┌──────────────────────────────────────────────────────────────────────┐
│                     Permission Seeding                                │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │ ICrudPermissionGenerator                                       │  │
│  │  → Scans assemblies for [AutoPermission] controllers          │  │
│  │  → Generates permission strings (incl. inherited CRUD methods)│  │
│  │  → Returns: ["create:products", "read:products", ...]        │  │
│  └────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────┘

API Reference

Attributes

AttributeTargetDescription
[AutoPermission("entity")]Class / MethodMarks controller for automatic permission generation and enforcement
[RequirePermission("action:entity")]MethodExplicit permission requirement (overrides convention)

AutoPermission Properties

PropertyTypeDefaultDescription
EntityNamestring(required)Entity name for the permission (normalized to lowercase)
ActionNamestring?nullCustom action name override (e.g., "export")
IncludeInPermissionsbooltrueWhether to include in permission generation/enforcement

Extension Methods

MethodDescription
services.AddGrydCrudAuth()Registers ICrudPermissionGenerator only
services.AddGrydCrudAuth(autoPermissions: true)Registers generator + CrudPermissionConvention
options.AddCrudPermissionConvention()Adds convention to MvcOptions directly

ICrudPermissionGenerator

MethodDescription
GeneratePermissions(params Assembly[])Scans assemblies for [AutoPermission] controllers and returns permission strings
GenerateStandardCrudPermissions(string entity)Returns 4 standard CRUD permissions for an entity

Released under the MIT License.