ConvergeERP.Shared.Authorization
1.0.1
dotnet add package ConvergeERP.Shared.Authorization --version 1.0.1
NuGet\Install-Package ConvergeERP.Shared.Authorization -Version 1.0.1
<PackageReference Include="ConvergeERP.Shared.Authorization" Version="1.0.1" />
<PackageVersion Include="ConvergeERP.Shared.Authorization" Version="1.0.1" />
<PackageReference Include="ConvergeERP.Shared.Authorization" />
paket add ConvergeERP.Shared.Authorization --version 1.0.1
#r "nuget: ConvergeERP.Shared.Authorization, 1.0.1"
#:package ConvergeERP.Shared.Authorization@1.0.1
#addin nuget:?package=ConvergeERP.Shared.Authorization&version=1.0.1
#tool nuget:?package=ConvergeERP.Shared.Authorization&version=1.0.1
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
- Quick Start
- Core Concepts
- Features
- Architecture Overview
- Interface Reference
- Complete Example
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
- Extracts the current request's HTTP method and path
- Looks up the endpoint policy from the cache
- If no policy found → Access Denied (deny by default)
- If
RequiresAuthorization = false→ Access Granted - If no permissions defined → Any authenticated user is granted access
- 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
- Own Company Access: By default, users have full access to their own company (
MyCompanyId == CompanyId) - Own Company with Rules: If rules exist for the user's own company, they restrict access to specific resources
- Other Company Access: By default, users have NO access to other companies
- Other Company with Rules: Explicit Allow rules must grant access to other companies
- 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:
GlobalBaseEntitytypes (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 | Versions 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. |
-
net10.0
- ClosedXML (>= 0.105.0)
- ConvergeERP.Shared.Domain (>= 2.0.2)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.1)
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 |
|---|