# RULE FILE 07 — RBAC & Permissions Rules

---

## Permission Architecture

Hybrid RBAC + ABAC (Role-Based + Attribute-Based). A user has a ROLE which gives them a SET of permissions. Each permission has a SCOPE that limits HOW FAR the permission reaches.

---

## Roles — The Complete Hierarchy

| Role | Level | Description | Scope Default |
|------|-------|-------------|---------------|
| `super_admin` | 100 | Platform owner. Cross-tenant. | all (bypasses tenant scope) |
| `academy_owner` | 90 | Academy owner. Full control within tenant. | academy |
| `academy_admin` | 80 | Administrator. Almost everything except billing/ownership. | academy |
| `branch_manager` | 70 | Manages one branch. | branch |
| `head_trainer` | 60 | Senior trainer. Manages trainers + groups. | branch |
| `trainer` | 50 | Conducts sessions. Takes attendance. | own_groups |
| `receptionist` | 40 | POS, payments, basic participant lookup. | branch |
| `accountant` | 35 | Financial reports, invoices, payments. No operations. | academy |
| `data_entry` | 30 | Creates/edits records. No financial access. | branch |
| `parent` | 5 | Views own children's data only. | own_children |

### Role Rules

1. Every user has EXACTLY ONE role (no multi-role per user)
2. Roles are seeded per academy at creation time
3. Role level determines hierarchy — higher level can manage lower levels
4. A user can ONLY assign roles with level LOWER than their own
5. `super_admin` is NOT per-academy — it's a platform-level flag (user.is_super_admin or separate mechanism)
6. Roles table: `id, academy_id, name, name_ar, level, is_system (bool), created_at, updated_at`
7. `is_system = true` roles cannot be deleted or renamed (the 10 above)
8. Academies CAN create custom roles (is_system=false) with custom permission sets

---

## Permissions — Module × Action × Scope

### Permission String Format

```
{module}.{action}
```

Examples: `participants.view`, `invoices.create`, `attendance.mark`, `settings.manage`

### Modules (exact strings)

```
participants, programs, groups, sessions, enrollments, attendance,
invoices, payments, wallets, cash_sessions, refunds, pricing_rules,
facilities, spaces, assignments, inventory, products, purchase_orders,
pos, kits, stock_counts, suppliers, warehouses,
users, roles, branches, settings, reports, notifications, templates,
evaluations, waitlists, audit_logs
```

### Actions (exact strings)

```
view, list, create, update, delete, export, import, approve, manage
```

- `view`: see a single record's details
- `list`: see the index/table of records
- `create`: add new records
- `update`: modify existing records
- `delete`: soft-delete or remove records
- `export`: download as PDF/Excel/CSV
- `import`: bulk upload from file
- `approve`: approve pending items (refunds, stock counts, excuses)
- `manage`: full CRUD + special operations (superset of all above)

### Scope (per permission assignment)

```
CHECK (scope IN ('own', 'own_groups', 'own_children', 'branch', 'academy', 'all'))
```

| Scope | Meaning | Query Filter |
|-------|---------|-------------|
| `own` | Only records created by this user | `WHERE created_by = auth()->id()` |
| `own_groups` | Only records in groups this user is assigned to | `WHERE training_group_id IN (user's assigned groups)` |
| `own_children` | Only records for this user's linked participants | `WHERE participant_id IN (guardian's children)` |
| `branch` | All records in user's branch | `WHERE branch_id = auth()->user()->branch_id` |
| `academy` | All records in the academy | (just tenant scope, no extra filter) |
| `all` | Cross-tenant (super_admin only) | No tenant scope applied |

---

## Permission Resolution Algorithm

```php
PermissionService::can(User $user, string $permission, ?Model $record = null): bool

// Step 1: Super admin check
if ($user->is_super_admin) return true;

// Step 2: Get user's role
$role = $user->role; // loaded with permissions

// Step 3: Check if role has this permission
$rolePermission = $role->permissions->firstWhere('name', $permission);
if (!$rolePermission) return false;

// Step 4: If no specific record to check, permission exists = allowed
if ($record === null) return true;

// Step 5: Scope check against the specific record
$scope = $rolePermission->pivot->scope;
return match ($scope) {
    'academy' => true, // tenant scope already applied by global scope
    'branch' => $record->branch_id === $user->branch_id,
    'own' => $record->created_by === $user->id,
    'own_groups' => $this->isInUserGroups($user, $record),
    'own_children' => $this->isUserChild($user, $record),
    'all' => true, // super admin already handled above
    default => false,
};
```

### Permission Table Structure

```
permissions: id, name, module, action, description, description_ar, created_at
permission_role: id, role_id, permission_id, scope, created_at
```

- `permission_role` is the pivot with the SCOPE column
- One role can have the same permission with different scopes? NO — one entry per role+permission
- The scope on the pivot determines HOW FAR this role's permission reaches

---

## Default Permission Matrix (System Roles)

### academy_owner (level 90)

ALL permissions with scope `academy`. Every module, every action.

### academy_admin (level 80)

All EXCEPT:
- `settings.manage` for billing/subscription (owner only)
- Cannot delete the academy itself
- Cannot change owner role

### branch_manager (level 70)

| Module | Permissions | Scope |
|--------|------------|-------|
| participants | view, list, create, update, export | branch |
| programs | view, list | academy |
| groups | view, list, create, update | branch |
| sessions | view, list, create, update, delete | branch |
| attendance | view, list, mark, export | branch |
| invoices | view, list, create | branch |
| payments | view, list, create | branch |
| cash_sessions | view, list, create, update | branch |
| inventory | view, list | branch |
| pos | manage | branch |
| users | view, list | branch |
| reports | view, export | branch |
| facilities | view, list, update | branch |
| assignments | view, list, create, update | branch |

### trainer (level 50)

| Module | Permissions | Scope |
|--------|------------|-------|
| participants | view, list | own_groups |
| groups | view | own_groups |
| sessions | view, list, update | own_groups |
| attendance | view, list, mark | own_groups |
| evaluations | view, list, create, update | own_groups |
| facilities | view | branch |

### receptionist (level 40)

| Module | Permissions | Scope |
|--------|------------|-------|
| participants | view, list, create, update | branch |
| invoices | view, list, create | branch |
| payments | view, list, create | branch |
| pos | manage | branch |
| cash_sessions | view, create, update | own |
| inventory | view, list | branch |
| products | view, list | branch |
| wallets | view | branch |

### parent (level 5)

| Module | Permissions | Scope |
|--------|------------|-------|
| participants | view | own_children |
| attendance | view | own_children |
| invoices | view, list | own_children |
| evaluations | view | own_children |
| programs | view, list | academy |
| wallets | view | own_children |

---

## Permission Enforcement Points

### 1. Route Middleware

```php
// web.php
Route::middleware(['auth', 'permission:participants.list'])->group(function () {
    Route::get('/participants', ParticipantList::class);
});
```

### 2. Livewire Component Mount

```php
public function mount()
{
    $this->authorize('participants.list');
}
```

### 3. Service Layer (for scope-sensitive operations)

```php
public function updateParticipant(Participant $participant, array $data): Participant
{
    // Service checks scope — can this user update THIS specific participant?
    if (!PermissionService::can(auth()->user(), 'participants.update', $participant)) {
        throw new UnauthorizedException();
    }
    // ... proceed
}
```

### 4. Blade Directives

```blade
@can('invoices.create')
    <x-ui.button wire:click="createInvoice">إنشاء فاتورة</x-ui.button>
@endcan
```

### 5. Query Scoping (automatic for list views)

```php
// In Livewire list components
$query = Participant::query();
$query = PermissionService::applyScope($query, auth()->user(), 'participants.list');
// Returns query with appropriate WHERE clause based on user's scope
```

---

## Impersonation Rules

1. Only `super_admin` and `academy_owner` can impersonate
2. Can only impersonate users with LOWER role level
3. Impersonation creates audit log entry: who, whom, when started, when ended
4. During impersonation: session stores both `impersonator_id` and `impersonated_id`
5. Yellow banner shown: "أنت تعمل كـ {username} — [العودة لحسابك]"
6. Impersonator retains ability to end impersonation at any time
7. All actions during impersonation are logged with BOTH user IDs
8. Cannot impersonate another super_admin
9. Cannot change password or security settings while impersonating

---

## Permission-Sensitive UI Rules

1. **Navigation**: only show menu items the user has permission to access
2. **Buttons**: only show action buttons (create, edit, delete) if user has that permission
3. **Table columns**: hide financial columns from non-financial roles
4. **Filters**: scope dropdown options to what the user can see (branch users don't see "all branches" option)
5. **Export buttons**: only if user has `{module}.export` permission
6. **Settings pages**: only accessible sections shown (not greyed out — hidden entirely)

---

## Permission Caching

- Permissions are loaded ONCE per request (eager-loaded with user's role)
- Cached in session for the request lifecycle
- Cache invalidated when: role permissions are modified, user's role is changed
- DO NOT cache permissions across requests in Redis/file — too many edge cases with real-time permission changes
