Appearance
RBAC & Permissions
Model Overview
mermaid
graph LR
USER["👤 User"] -->|"has one"| ROLE["🏷️ Role"]
ROLE -->|"linked to many"| RPOL["Role Policies"]
USER -->|"direct-assigned"| UPOL["User Policies"]
RPOL -->|"contains"| POLICY["📋 Policy"]
UPOL -->|"contains"| POLICY
POLICY -->|"bundles many"| PERM["🔑 Permission<br/>Collection:Action"]
style PERM fill:#16a34a,color:#fff
style POLICY fill:#d97706,color:#fff
style ROLE fill:#7c3aed,color:#fff
style USER fill:#2563eb,color:#fffPermissions are resolved and merged from both sources:
- Policies attached to the user's role
- Policies attached directly to the user
The result is a deduplicated flat list of Collection:Action strings encoded as perm claims in the JWT.
Permission Format
Every permission is a Collection:Action pair:
| Collection | Possible Actions |
|---|---|
Vendor | Read, Create, Update, Delete |
Invoice | Read, Create, Update, Delete |
Transaction | Read, Create, Update, Delete |
Dashboard | Read |
Setting | Read, Update |
User | Read, Create, Update, Delete |
Role | Read, Create, Update, Delete |
Permission | Read |
Policy | Read, Create, Update, Delete |
Report | Read |
Admin Bypass
When is_admin = "true" is present in the JWT, all permission checks are skipped entirely.
Admin users are identified at login time — the is_admin flag is stored on the User record in the database.
Enforcing Permissions
Backend: [RequirePermission]
Every controller action (except onboarding) is decorated:
csharp
[HttpGet]
[RequirePermission("Vendor", "Read")]
public async Task<ActionResult<PagedResult<VendorDto>>> GetVendors(...)The PermissionAuthorizationFilter inspects the JWT claims and returns 403 Forbidden if the required claim is absent.
Frontend: usePermission()
typescript
// In any Vue component or composable
const { can } = usePermission()
// Conditionally render a button
if (can('Vendor', 'Create')) { ... }The composable reads the decoded JWT from the auth store and performs the same Collection:Action check client-side.
AppPermissionGate Component
Wrap any UI element to hide it from users without the required permission:
vue
<AppPermissionGate collection="Vendor" action="Create">
<AppButton @click="openCreateModal">New Vendor</AppButton>
</AppPermissionGate>System Roles
Roles seeded in migrations with is_system = true cannot be deleted or deactivated through the API.
| Role | Description |
|---|---|
Admin | Full access — is_admin = true on all users assigned this role |
Pharmacist | Standard operator — access to vendor management features |
Defining New Permissions
New permissions must be seeded in a migration script in MedRegia.Migrations/Scripts/Seed/:
sql
INSERT INTO permissions (collection, action, description) VALUES
('NewEntity', 'Read', 'View new entities'),
('NewEntity', 'Create', 'Create new entities'),
('NewEntity', 'Update', 'Update new entities'),
('NewEntity', 'Delete', 'Delete new entities');Then attach them to the appropriate policy:
sql
INSERT INTO policy_permissions (policy_id, permission_id)
SELECT p.id, pm.id
FROM policies p, permissions pm
WHERE p.name = 'Pharmacist Full Access'
AND pm.collection = 'NewEntity';