Skip to content

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:#fff

Permissions are resolved and merged from both sources:

  1. Policies attached to the user's role
  2. 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:

CollectionPossible Actions
VendorRead, Create, Update, Delete
InvoiceRead, Create, Update, Delete
TransactionRead, Create, Update, Delete
DashboardRead
SettingRead, Update
UserRead, Create, Update, Delete
RoleRead, Create, Update, Delete
PermissionRead
PolicyRead, Create, Update, Delete
ReportRead

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.

RoleDescription
AdminFull access — is_admin = true on all users assigned this role
PharmacistStandard 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';

MedRegia — Pharmaceutical Vendor Management Platform