Roles & Permissions
RBAC system with scope override, field-level restrictions, anti-escalation, and tenant-scoped roles.
SCRIPE implements a comprehensive, highly optimized RBAC (Role-Based Access Control) system with modular category-based permissions, scope overrides, field-level restrictions (FLS), and tenant scoping. Permissions are dynamically loaded from providers, cached server-side using IMemoryCache, and validated via programmatic checkers.
Permission Hierarchy
Permission System
Permissions follow a strict {Resource}.{Action} naming convention. Instead of static declarations, each backend module defines its permissions by implementing IModulePermissionProvider (e.g., IdentityPermissionProvider, CompliancePermissionProvider). On startup, these providers are automatically discovered, and the DatabaseSeeder uses PermissionSeeder.SyncFromProvidersAsync to synchronize and seed permissions in the database.
| Permission Category | Permissions | Format |
|---|---|---|
| Admin Management | Create, Read, Update, Delete, Block, Impersonate, Transfer | admins.view, admins.create, admins.impersonate, admins.transfer |
| Role Management | Create, Read, Update, Delete, Assign Permissions, Clone Role | roles.view, roles.create, roles.manage_permissions, roles.clone |
| Tenant Management | Create, Read, Update, Delete, Settings, Drill-Down, Cascade Delete | tenants.view, tenants.create, tenants.drill_down, tenants.cascade_delete |
| Compliance & Privacy | Regulations, Consent Management, DSR Create/Review/Execute | compliance_regulations.view, compliance_dsr.execute, compliance_consent.manage |
| Audit Logs | Read, Export, View Children | audit.view, audit.export, audit.view_children |
| Dashboard | View Stats, View Charts, View Security Feed | dashboard.view, analytics.view, security.view |
Scope Override (Data Access Control)
Each RolePermission can override the default scope of a permission via the ScopeOverride field. The DataScopeService resolves the effective scope using a strict priority list: 1) Drill-down Context (ContextTenantId claim), 2) RolePermission Scope Override, 3) God-Mode Check (SystemProtectedAdmin), 4) Hierarchy Flag (IncludeChildTenants), 5) Tenant Assigned, 6) Global Fallback. When multiple roles are assigned, AdminSecurityService.GetWidestScope resolves the widest scope: all_tenants > hierarchy > own_tenant > own. To prevent privilege leakage, tenant administrators are strictly capped to their own sub-hierarchy during scope resolution.
public class RolePermission : AuditableEntity
{
public Guid RoleId { get; set; }
public Guid PermissionId { get; set; }
// Override the permission's default scope for THIS role
public string? ScopeOverride { get; set; }
// JSON array of field names hidden for this role
public string? RestrictedFieldsJson { get; set; }
// Example: ["salary", "ssn", "bankAccount"]
// Navigation
public virtual Role Role { get; set; } = null!;
public virtual Permission Permission { get; set; } = null!;
}| Scope Value | Data Access Description | Use Case / Scoping Boundary |
|---|---|---|
| all_tenants | System-wide access across all tenants in the system | Super admin global operations |
| context_tenant | Data restricted to a specific tenant context (drill-down mode) | Super admin tenant drill-down |
| hierarchy | Data from own tenant + all descendant child tenants | Branch manager overseeing sub-tenants |
| own_tenant | Data from own tenant only (default isolation boundary) | Default for tenant-level administrators |
| own | Only data created by the current user (CreatedBy == UserId) | Individual team members seeing only their entries |
Authorization Pipeline
Authorization is decoupled from static configurations. The DynamicPermissionPolicyProvider dynamically constructs ASP.NET Core authorization policies for routes containing the [PermissionRequired] attribute. To keep JWT token size under 400 bytes, user permissions are not stored in claims but are cached server-side using AdminPermissionCache (with a 10-minute sliding expiration). Caching utilizes a central CancellationTokenSource for thread-safe global eviction upon any role modification. Programmatic checks are performed in handlers and services via the IPermissionChecker interface.
// Route-level and pipeline authorization attributes
// 1. Dynamic RBAC verification with dynamic policy generation
[PermissionRequired("admins.view")]
// 2. Requires active administrator token
[AdminOnly]
// 3. Requires standard client/user token
[UserOnly]
// 4. Rate limiting policies applied per-user or at auth endpoints
[EnableRateLimiting("per-user")]
[EnableRateLimiting("login")]Field-Level Restrictions
Field-Level Security (FLS) allows administrators to restrict specific fields of an entity for specific roles. Restricted fields are configured as a JSON array of string paths (e.g., ["salary", "ssn"]) and stored in the RestrictedFieldsJson column (varchar/nvarchar/VARCHAR2 up to 2000 chars) of the RolePermission table. During execution, the RestrictedFieldsAuthorizationFilter identifies the target resource and sets HttpContext.Items["RestrictedFields"]. The FieldProjectionMiddleware intercepts HTTP 2xx JSON responses, parses the body into a JsonNode tree, and recursively nullifies restricted properties matching the exact or relative path (e.g., address.street) to prevent reflection overhead.
// RolePermission for role "HR_VIEWER" on permission "admins.view"
{
"scopeOverride": "own_tenant",
"restrictedFieldsJson": "["salary", "ssn", "bankAccount", "nationalId"]"
}
// API Response: restricted fields are recursively nullified by middleware
{
"id": "abc-123",
"name": "John Doe",
"email": "john@example.com",
"salary": null, // Nullified
"ssn": null, // Nullified
"bankAccount": null // Nullified
}private void FilterNode(JsonNode? node, HashSet<string> restrictedFields, string path)
{
if (node == null) return;
if (node is JsonObject obj)
{
var keysToNullify = new List<string>();
foreach (var prop in obj)
{
var fieldPath = string.IsNullOrEmpty(path) ? prop.Key : path + "." + prop.Key;
if (restrictedFields.Contains(prop.Key, StringComparer.OrdinalIgnoreCase) ||
restrictedFields.Contains(fieldPath, StringComparer.OrdinalIgnoreCase))
{
keysToNullify.Add(prop.Key);
}
else
{
FilterNode(prop.Value, restrictedFields, fieldPath);
}
}
foreach (var key in keysToNullify)
{
obj[key] = null; // Zero-reflection nullification
}
}
else if (node is JsonArray arr)
{
foreach (var item in arr)
{
FilterNode(item, restrictedFields, path);
}
}
}Clone Role (Anti-Escalation)
To prevent privilege escalation, the CloneRoleCommandHandler filters the copied permission list against the cloner's own active permissions, silently dropping any unpossessed permissions. In the permission assignment command, attempting to explicitly add permissions that the administrator does not possess throws a Forbidden role.permissionEscalation error. Additionally, the TenantGuardianService validates all role updates and blocks any changes to locked system-critical roles (IsPermissionLocked == true).
Role Entity Properties
Each role has several system flags that control its behavior and protection level.
| Property | Type | Purpose |
|---|---|---|
| NameEn / NameAr | string | Bilingual display name |
| Code | string | Unique identifier (e.g. ACME_SUPER_ADMIN) |
| TenantId | Guid | Scoped to a specific tenant |
| IsSystem | bool | Seeded role, cannot be modified |
| IsDeletable | bool | System roles are protected from deletion |
| IsPermissionLocked | bool | Super admin roles permissions cannot be changed |
| IsTenantSuperAdmin | bool | Exactly one per tenant, gets all tenant permissions |
| IsDefaultRole | bool | Automatically assigned to new users/admins |
| Priority | int | Lower = higher priority. Controls role hierarchy. |
Role API Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v1/roles | List all roles (data-scope aware) | 🔒 Required |
| GET | /api/v1/roles/{id} | Get role with permissions | 🔒 Required |
| GET | /api/v1/roles/{id}/admin-count | Count admins with this role (delete warning) | 🔒 Required |
| GET | /api/v1/roles/{id}/permissions | Get role's assigned permissions with scopes | 🔒 Required |
| POST | /api/v1/roles | Create new role (with TenantId) | 🔒 Required |
| PUT | /api/v1/roles/{id} | Update role details | 🔒 Required |
| DELETE | /api/v1/roles/{id} | Delete role (if IsDeletable) | 🔒 Required |
| PUT | /api/v1/roles/{id}/permissions | Assign permissions with scope overrides | 🔒 Required |
| DELETE | /api/v1/roles/{id}/permissions/{permId} | Remove single permission from role | 🔒 Required |
| POST | /api/v1/roles/{id}/clone | Clone role (anti-escalation) | 🔒 Required |
My Tenant Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v1/roles/my-tenant | Roles in current user's tenant (from JWT) | 🔒 Required |
| GET | /api/v1/roles/my-tenant/available-permissions | Permissions available for role assignment (filtered by tenant pool) | 🔒 Required |
| POST | /api/v1/roles/my-tenant | Create role for current tenant | 🔒 Required |
Permission Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/v1/permissions | List all permissions grouped by category | 🔒 Required |
| GET | /api/v1/permissions/categories | Get permission category list | 🔒 Required |
| GET | /api/v1/permissions/flat | Flat list of all permissions | 🔒 Required |