ConvergeERP.Shared.Authorization 1.0.1

The owner has unlisted this package. This could mean that the package is deprecated, has security vulnerabilities or shouldn't be used anymore.
dotnet add package ConvergeERP.Shared.Authorization --version 1.0.1
                    
NuGet\Install-Package ConvergeERP.Shared.Authorization -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ConvergeERP.Shared.Authorization" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ConvergeERP.Shared.Authorization" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="ConvergeERP.Shared.Authorization" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ConvergeERP.Shared.Authorization --version 1.0.1
                    
#r "nuget: ConvergeERP.Shared.Authorization, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ConvergeERP.Shared.Authorization@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ConvergeERP.Shared.Authorization&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=ConvergeERP.Shared.Authorization&version=1.0.1
                    
Install as a Cake Tool

ConvergeERP.Shared.Authorization

A comprehensive authorization infrastructure library for ConvergeERP 2.0 microservices. This library provides JWT authentication, dynamic endpoint policy authorization, entity-level CRUD permissions, resource-level access control, and multi-tenant scope filtering.

Table of Contents


Installation

dotnet add package ConvergeERP.Shared.Authorization

Quick Start

1. Implement and Register Cache (Required)

You must implement IAuthorizationCache and register it before calling AddConvergeAuthorization:

public class RedisAuthorizationCache : IAuthorizationCache
{
    private readonly IDistributedCache _cache;

    public RedisAuthorizationCache(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
    {
        var data = await _cache.GetStringAsync(key, cancellationToken);
        return data is null ? default : JsonSerializer.Deserialize<T>(data);
    }

    public async Task SetAsync<T>(string key, T value, CancellationToken cancellationToken = default)
    {
        var data = JsonSerializer.Serialize(value);
        await _cache.SetStringAsync(key, data, cancellationToken);
    }
}

2. Register Services

// Program.cs

// Register Redis distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});

// Register cache implementation (REQUIRED - must be before AddConvergeAuthorization)
builder.Services.AddSingleton<IAuthorizationCache, RedisAuthorizationCache>();

// Register authorization services
builder.Services.AddConvergeAuthorization(
    builder.Configuration,
    serviceName: "InventoryService",
    permissionsExcelPath: "Permissions/permissions.xlsx"); // Optional

3. Add Middleware

// In your middleware pipeline (after UseRouting, before MapControllers)
app.UseRouting();
app.UseConvergeAuthorization();
app.MapControllers();

4. Configure JWT Settings

Add to your appsettings.json:

{
  "JwtSettings": {
    "IssuerSigningKey": "your-base64-encoded-signing-key",
    "ValidIssuer": "your-issuer",
    "ValidAudience": "your-audience"
  }
}

Core Concepts

Authorization Layers

This library implements a multi-layered authorization approach:

Layer Interface Purpose
Endpoint [Authorize(Policy = "EndpointPolicy")] Controls access to API endpoints based on feature permissions
Entity IEntityAccessEvaluator<T> Controls CRUD operations on entity types
Resource IAccessRulesEvaluator Controls access to specific entity instances via Allow/Deny rules
Scope IScopeFilter Filters data by tenant/company context
Domain DomainAuthorizationRule<T> Custom business logic authorization

Multi-Tenant Architecture

The library supports a hierarchical multi-tenant architecture:

Global (TenantId = null)
  └── Tenant (TenantId = X)
        └── Company (CompanyId = Y)

User Scope

Users can operate at three scope levels:

Scope Description Access Rules Enforced
Global Access across all tenants (TenantId = null) ❌ No
Tenant Access within a specific tenant ❌ No
Company Access within a specific company ✅ Yes

Features

JWT Authentication

JWT Bearer authentication is automatically configured when you call AddConvergeAuthorization.

Required Claims

The library expects the following claims in JWT tokens:

Claim Type Description
sub Guid User ID
email string User email
name string Display name
Phone string Phone number
TenantId Guid Tenant identifier
CompanyId Guid User's own company ID
IsTenantAdmin bool Tenant admin flag
Permissions string[] Feature permissions (e.g., "Inventory.Products.Create")
ResourceAccess JSON Allow/Deny resource access rules
EntityAccess JSON Entity-level CRUD permissions
Company Context Header

Users can switch companies by sending the Company_Id header:

GET /api/products HTTP/1.1
Company_Id: 550e8400-e29b-41d4-a716-446655440000

Current User Context (ICurrentUser)

Access authenticated user information anywhere via dependency injection:

public class ProductService
{
    private readonly ICurrentUser _currentUser;

    public ProductService(ICurrentUser currentUser)
    {
        _currentUser = currentUser;
    }

    public void DoSomething()
    {
        var userId = _currentUser.UserId;
        var tenantId = _currentUser.TenantId;
        var companyId = _currentUser.CompanyId;      // Current company context
        var myCompanyId = _currentUser.MyCompanyId;  // User's own company
        var permissions = _currentUser.Permissions;
        var scope = _currentUser.Scope;
    }
}
Available Properties
Property Type Description
UserId Guid? Authenticated user's unique identifier
Email string User's email address
Name string User's display name
Phone string User's phone number
TenantId Guid? Tenant identifier
MyCompanyId Guid? User's own company ID
CompanyId Guid? Current company context (from header or token)
Roles List<string> Assigned roles
Permissions List<string> Feature permissions
Resources Dictionary<string, List<ResourceAccess>> Allow/Deny resource rules
EntityAccess List<EntityAccess> Entity CRUD permissions
IsTenantAdmin bool Whether user is a tenant administrator
Scope UserScope Current scope (Global, Tenant, or Company)
Changing Scope

Temporarily change the user's scope using the disposable pattern:

// Temporarily elevate to tenant scope
using (_currentUser.ChangeScope(UserScope.Tenant))
{
    // Operations here run at tenant scope
    // Access rules are NOT enforced
}
// Scope automatically reverts when disposed

Dynamic Endpoint Policy Authorization

Authorize API endpoints dynamically based on policies stored in a distributed cache.

Apply to Controllers
[Authorize(Policy = "EndpointPolicy")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> Create(CreateProductRequest request)
    {
        // Checks if user has permissions for "POST /api/products"
    }

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        // Checks permissions for "GET /api/products"
    }
}
Authorization Logic
  1. Extracts the current request's HTTP method and path
  2. Looks up the endpoint policy from the cache
  3. If no policy found → Access Denied (deny by default)
  4. If RequiresAuthorization = false → Access Granted
  5. If no permissions defined → Any authenticated user is granted access
  6. If permissions defined → User must have at least ONE of them
Receiving Policy Updates

Implement a consumer to receive policy updates from the Identity Service:

public class PolicyUpdateConsumer : BackgroundService
{
    private readonly IEndpointPolicyStore _policyStore;

    public PolicyUpdateConsumer(IEndpointPolicyStore policyStore)
    {
        _policyStore = policyStore;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // When policies are received from message broker:
        var policies = JsonSerializer.Deserialize<List<EndpointPolicy>>(message);
        await _policyStore.UpdatePolicies(policies);
        // Only policies matching this service's name are stored
    }
}

Entity Access Evaluation (IEntityAccessEvaluator)

Check if the current user can perform CRUD operations on entity types.

public class ProductService
{
    private readonly IEntityAccessEvaluator<Product> _entityAccess;

    public ProductService(IEntityAccessEvaluator<Product> entityAccess)
    {
        _entityAccess = entityAccess;
    }

    public async Task CreateProductAsync(Product product)
    {
        if (!_entityAccess.CanCreate())
            throw new UnauthorizedAccessException("Cannot create products");

        // Create product...
    }

    public async Task<Product> GetProductAsync(Guid id)
    {
        if (!_entityAccess.CanRead())
            throw new UnauthorizedAccessException("Cannot read products");

        // Get product...
    }
}
Available Methods
Method Description
CanCreate() Check if user can create new instances
CanRead() Check if user can read instances
CanUpdate() Check if user can update instances
CanDelete() Check if user can delete instances

Resource Access Rules (IAccessRulesEvaluator)

Filter collections or check access to specific entity instances based on Allow/Deny rules.

Business Rules
  1. Own Company Access: By default, users have full access to their own company (MyCompanyId == CompanyId)
  2. Own Company with Rules: If rules exist for the user's own company, they restrict access to specific resources
  3. Other Company Access: By default, users have NO access to other companies
  4. Other Company with Rules: Explicit Allow rules must grant access to other companies
  5. Deny Precedence: Deny rules always override Allow rules
Usage Examples
public class ProductService
{
    private readonly IAccessRulesEvaluator _accessRulesEvaluator;

    public ProductService(IAccessRulesEvaluator accessRulesEvaluator)
    {
        _accessRulesEvaluator = accessRulesEvaluator;
    }

    // Filter a collection to only accessible items
    public IEnumerable<Product> GetAccessibleProducts(IEnumerable<Product> products)
    {
        return _accessRulesEvaluator.FilterByAccessRules(products);
    }

    // Check if a specific item is accessible
    public Product GetProduct(Product product)
    {
        if (!_accessRulesEvaluator.CheckIfAccessible(product))
            throw new UnauthorizedAccessException("Access denied to this product");
        
        return product;
    }
}
ResourceAccess Claim Structure
{
  "Allow": [
    {
      "Entity": "Product",
      "CompanyOptions": "Selected",
      "Companies": ["company-guid-1", "company-guid-2"],
      "AccessAll": false,
      "ResourceIds": ["product-guid-1", "product-guid-2"]
    }
  ],
  "Deny": [
    {
      "Entity": "Product",
      "CompanyOptions": "All",
      "AccessAll": false,
      "ResourceIds": ["product-guid-3"]
    }
  ]
}

Scope Filtering (IScopeFilter)

Filter entity queries based on the current user's scope (Global, Tenant, or Company).

public class ProductRepository
{
    private readonly IScopeFilter _scopeFilter;
    private readonly DbContext _context;

    public ProductRepository(IScopeFilter scopeFilter, DbContext context)
    {
        _scopeFilter = scopeFilter;
        _context = context;
    }

    // For IQueryable (database-level filtering)
    public IQueryable<Product> GetProducts()
    {
        return _scopeFilter.ApplyScopeFilter(_context.Products);
    }

    // For in-memory collections
    public IEnumerable<Product> FilterProducts(IEnumerable<Product> products)
    {
        return _scopeFilter.FilterByScope(products);
    }

    // Check single entity accessibility
    public bool IsAccessible(Product product)
    {
        return _scopeFilter.IsAccessibleAtScope(product);
    }
}
Filtering Rules
Scope Filter Applied
Global TenantId == Guid.Empty (global entities only)
Tenant TenantId == CurrentUser.TenantId
Company CompanyId == CurrentUser.CompanyId

Note: GlobalBaseEntity types (without TenantId/CompanyId) are not filtered.


Domain Authorization Rules

Create custom authorization rules for complex business logic:

public class CanEditOrderRule : DomainAuthorizationRule<Order>
{
    public CanEditOrderRule(ICurrentUser currentUser) : base(currentUser) { }

    public override async Task EvaluateAsync(Order? resource, CancellationToken ct)
    {
        if (resource == null)
            throw new ArgumentNullException(nameof(resource));

        // Only order owner or tenant admin can edit
        if (resource.CreatedByUserId != CurrentUser.UserId && !CurrentUser.IsTenantAdmin)
            throw new UnauthorizedAccessException("You cannot edit this order.");
    }
}

// Usage in a service
public class OrderService
{
    private readonly CanEditOrderRule _canEditRule;

    public OrderService(CanEditOrderRule canEditRule)
    {
        _canEditRule = canEditRule;
    }

    public async Task UpdateOrderAsync(Order order, CancellationToken ct)
    {
        await _canEditRule.EvaluateAsync(order, ct);
        // Update order...
    }
}

Permission Excel Import & Publishing

Services can define their permissions in an Excel file and publish them to the Identity Service on startup.

Excel File Structure

Create an Excel file with the following sheets:

1. Roles Sheet
Column Description
Name Role name (e.g., "Admin", "Manager")
Description Role description

Example:

Name Description
Admin Full system access
Manager Department management access
Viewer Read-only access
2. RolePermissions Sheet
Column Description
Role Role name (e.g., "Admin", "Manager")
Module Module name (e.g., "Inventory")
Feature Feature name (e.g., "Products")
Action Action name (e.g., "Create", "Read")

Example:

Role Module Feature Action
Admin Inventory Products Create
Admin Inventory Products Read
Admin Inventory Products Update
Admin Inventory Products Delete
Manager Inventory Products Create
Manager Inventory Products Read
Viewer Inventory Products Read
3. EntityPermissions Sheet
Column Description
Role Role name
Entity Entity class name
Create Boolean - can create
Read Boolean - can read
Update Boolean - can update
Delete Boolean - can delete

Example:

Role Entity Create Read Update Delete
Admin Product TRUE TRUE TRUE TRUE
Manager Product TRUE TRUE TRUE FALSE
Viewer Product FALSE TRUE FALSE FALSE
4. PermissionPolicies Sheet
Column Description
Endpoint HTTP method and path (e.g., "POST /api/products")
Permissions Comma-separated permission names
RequiresAuthorization Boolean - whether auth is required

Example:

Endpoint Permissions RequiresAuthorization
POST /api/products Inventory.Products.Create TRUE
GET /api/products Inventory.Products.Read TRUE
GET /api/products/{id} Inventory.Products.Read TRUE
PUT /api/products/{id} Inventory.Products.Update TRUE
DELETE /api/products/{id} Inventory.Products.Delete TRUE
GET /api/health FALSE
Publishing to Identity Service

Implement IPermissionImportPublisher to publish permissions via your message broker:

public class RabbitMqPermissionPublisher : IPermissionImportPublisher
{
    private readonly IConnection _connection;

    public RabbitMqPermissionPublisher(IConnection connection)
    {
        _connection = connection;
    }

    public async Task PublishAsync(PermissionImportResult result, CancellationToken cancellationToken = default)
    {
        using var channel = await _connection.CreateChannelAsync(cancellationToken);
        
        var message = JsonSerializer.SerializeToUtf8Bytes(result);
        
        await channel.BasicPublishAsync(
            exchange: "converge.authorization",
            routingKey: PermissionImportResult.DefaultRoutingKey,
            body: message,
            cancellationToken: cancellationToken);
    }
}

// Register BEFORE calling AddConvergeAuthorization
builder.Services.AddSingleton<IPermissionImportPublisher, RabbitMqPermissionPublisher>();
Message Routing Keys
Message Type Routing Key
Permission Import converge.authorization.permissions-import
Endpoint Policies converge.authorization.endpoint-policies

Architecture Overview

Service Startup Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Service Startup                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│  1. Excel file is parsed by PermissionExcelImporter (tagged with serviceName)
│  2. PermissionImportPublisherService publishes PermissionImportResult        │
│  3. Identity Service receives and seeds permissions into database            │
│  4. Identity Service publishes EndpointPolicy updates                        │
│  5. Services receive policies and store only those matching their serviceName│
│  6. Policies are persisted to distributed cache (Redis, etc.)                │
└─────────────────────────────────────────────────────────────────────────────┘

Request Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Request Flow                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│  1. JWT validated by authentication middleware                               │
│  2. GetCurrentUserMiddleware extracts claims into ICurrentUser               │
│  3. EndpointPolicyAuthorizationHandler checks endpoint permissions from cache│
│  4. IScopeFilter filters data by tenant/company scope                        │
│  5. IEntityAccessEvaluator checks CRUD permissions                           │
│  6. IAccessRulesEvaluator filters resources by Allow/Deny rules              │
│  7. DomainAuthorizationRule evaluates business-specific authorization        │
└─────────────────────────────────────────────────────────────────────────────┘

Interface Reference

Public Interfaces

Interface Required Purpose
IAuthorizationCache ✅ Required Provide distributed cache for endpoint policies. Must be implemented by consumer.
ICurrentUser Auto-registered Access authenticated user's claims, permissions, and scope.
IEntityAccessEvaluator<T> Auto-registered Check CRUD permissions for entity types.
IAccessRulesEvaluator Auto-registered Filter entities by Allow/Deny resource access rules.
IScopeFilter Auto-registered Filter entities by tenant/company scope.
IEndpointPolicyStore Auto-registered Update endpoint policies from message broker.
IPermissionImportPublisher Optional Publish permissions to Identity Service. Implement to enable Excel import.

Extension Methods

Method Purpose
AddConvergeAuthorization(configuration, serviceName, excelPath?) Register all authorization services
UseConvergeAuthorization() Add authentication, current user, and authorization middleware

Complete Example

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

// 1. Register Redis distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});

// 2. Register cache implementation (REQUIRED)
builder.Services.AddSingleton<IAuthorizationCache, RedisAuthorizationCache>();

// 3. Register permission publisher (optional - for Excel import)
builder.Services.AddSingleton<IPermissionImportPublisher, RabbitMqPermissionPublisher>();

// 4. Add authorization services
builder.Services.AddConvergeAuthorization(
    builder.Configuration,
    serviceName: "InventoryService",
    permissionsExcelPath: "Permissions/permissions.xlsx");

// 5. Register policy update consumer
builder.Services.AddHostedService<RabbitMqPolicyConsumer>();

var app = builder.Build();

// 6. Configure middleware pipeline
app.UseRouting();
app.UseConvergeAuthorization();
app.MapControllers();

app.Run();

Example Controller with Full Authorization

[Authorize(Policy = "EndpointPolicy")]
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ICurrentUser _currentUser;
    private readonly IEntityAccessEvaluator<Product> _entityAccess;
    private readonly IAccessRulesEvaluator _accessRules;
    private readonly IScopeFilter _scopeFilter;
    private readonly IProductRepository _repository;

    public ProductsController(
        ICurrentUser currentUser,
        IEntityAccessEvaluator<Product> entityAccess,
        IAccessRulesEvaluator accessRules,
        IScopeFilter scopeFilter,
        IProductRepository repository)
    {
        _currentUser = currentUser;
        _entityAccess = entityAccess;
        _accessRules = accessRules;
        _scopeFilter = scopeFilter;
        _repository = repository;
    }

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        // 1. Endpoint policy already checked by [Authorize(Policy = "EndpointPolicy")]
        
        // 2. Check entity-level read permission
        if (!_entityAccess.CanRead())
            return Forbid();

        // 3. Get products filtered by scope (tenant/company)
        var products = await _repository.GetAllAsync();
        var scopeFiltered = _scopeFilter.FilterByScope(products);

        // 4. Apply resource access rules (Allow/Deny)
        var accessFiltered = _accessRules.FilterByAccessRules(scopeFiltered);

        return Ok(accessFiltered);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(Guid id)
    {
        if (!_entityAccess.CanRead())
            return Forbid();

        var product = await _repository.GetByIdAsync(id);
        
        if (product == null)
            return NotFound();

        // Check scope and access rules for single item
        if (!_scopeFilter.IsAccessibleAtScope(product))
            return Forbid();

        if (!_accessRules.CheckIfAccessible(product))
            return Forbid();

        return Ok(product);
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductRequest request)
    {
        if (!_entityAccess.CanCreate())
            return Forbid();

        var product = new Product
        {
            Name = request.Name,
            TenantId = _currentUser.TenantId!.Value,
            CompanyId = _currentUser.CompanyId!.Value
        };

        await _repository.AddAsync(product);
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
}

License

This package is proprietary software for ConvergeERP 2.0.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated