# ISP360 Authentication & RBAC Specification

**Version:** 1.0  
**Date:** 2026-06-13  
**Prepared by:** Polakis Technologies  

Companion: [DATABASE_DESIGN.md](DATABASE_DESIGN.md) | [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) | [UI_SPECIFICATIONS.md](UI_SPECIFICATIONS.md)

---

## 1. Overview

ISP360 implements **permission-based RBAC** (not page-based). Access is enforced via:

1. `permission_check()` — granular action permissions (`customers.create`)
2. `role_check()` — role membership (`isp_admin`)
3. `tenant_context()` — tenant isolation on every query
4. `audit_log()` — immutable audit trail on sensitive actions

### Authentication Surfaces

| Surface | Session Type | Table |
|---------|--------------|-------|
| Admin panel | Staff session | `sessions` |
| Customer portal | Portal session | `portal_sessions` |
| REST API | Bearer token | `api_tokens` |
| Router/integration | Service token | `api_tokens` (type=service) |

---

## 2. Roles

| Role Code | Display Name | Type | tenant_id | Description |
|-----------|--------------|------|-----------|-------------|
| `super_admin` | Super Admin | platform | NULL | Full platform access; cross-tenant; users.tenant_id IS NULL |
| `isp_admin` | ISP Admin | tenant | tenant-scoped | Full tenant access; all modules enabled for tenant |
| `support_agent` | Support Agent | tenant | tenant-scoped | Customers, tickets, read-only billing/network |
| `finance_user` | Finance User | tenant | tenant-scoped | Billing, payments, invoices, reports; no network write |
| `technician` | Technician | tenant | tenant-scoped | Routers, VPN, RADIUS, PPPoE, hotspot; no billing write |
| `customer` | Customer | customer | tenant-scoped | Customer portal only; portal_* pages |

### Role Hierarchy

```
Super Admin (platform)
  └── ISP Admin (tenant owner)
        ├── Finance User
        ├── Technician
        └── Support Agent
Customer (portal — separate auth context)
```

---

## 3. Permission Catalog

**Total permissions:** 182

### Module: `tenants`

| Permission Code | Action |
|-----------------|--------|
| `tenants.view` | View |
| `tenants.create` | Create |
| `tenants.edit` | Edit |
| `tenants.delete` | Delete |
| `tenants.suspend` | Suspend |
| `tenants.impersonate` | Impersonate |
| `tenants.billing` | Billing |

### Module: `users`

| Permission Code | Action |
|-----------------|--------|
| `users.view` | View |
| `users.create` | Create |
| `users.edit` | Edit |
| `users.delete` | Delete |
| `users.reset_password` | Reset Password |
| `users.assign_roles` | Assign Roles |
| `users.view_audit` | View Audit |

### Module: `roles`

| Permission Code | Action |
|-----------------|--------|
| `roles.view` | View |
| `roles.create` | Create |
| `roles.edit` | Edit |
| `roles.delete` | Delete |
| `roles.assign_permissions` | Assign Permissions |

### Module: `customers`

| Permission Code | Action |
|-----------------|--------|
| `customers.view` | View |
| `customers.create` | Create |
| `customers.edit` | Edit |
| `customers.delete` | Delete |
| `customers.suspend` | Suspend |
| `customers.export` | Export |
| `customers.import` | Import |
| `customers.kyc_verify` | Kyc Verify |
| `customers.view_documents` | View Documents |

### Module: `customer_groups`

| Permission Code | Action |
|-----------------|--------|
| `customer_groups.view` | View |
| `customer_groups.create` | Create |
| `customer_groups.edit` | Edit |
| `customer_groups.delete` | Delete |

### Module: `packages`

| Permission Code | Action |
|-----------------|--------|
| `packages.view` | View |
| `packages.create` | Create |
| `packages.edit` | Edit |
| `packages.delete` | Delete |
| `packages.publish` | Publish |

### Module: `billing`

| Permission Code | Action |
|-----------------|--------|
| `billing.view` | View |
| `billing.create` | Create |
| `billing.edit` | Edit |
| `billing.void` | Void |
| `billing.send` | Send |
| `billing.export` | Export |

### Module: `invoices`

| Permission Code | Action |
|-----------------|--------|
| `invoices.view` | View |
| `invoices.create` | Create |
| `invoices.edit` | Edit |
| `invoices.void` | Void |
| `invoices.send` | Send |
| `invoices.download` | Download |

### Module: `payments`

| Permission Code | Action |
|-----------------|--------|
| `payments.view` | View |
| `payments.create` | Create |
| `payments.edit` | Edit |
| `payments.refund` | Refund |
| `payments.reverse` | Reverse |
| `payments.reconcile` | Reconcile |
| `payments.allocate` | Allocate |
| `payments.export` | Export |

### Module: `credit_notes`

| Permission Code | Action |
|-----------------|--------|
| `credit_notes.view` | View |
| `credit_notes.create` | Create |
| `credit_notes.edit` | Edit |
| `credit_notes.void` | Void |

### Module: `debit_notes`

| Permission Code | Action |
|-----------------|--------|
| `debit_notes.view` | View |
| `debit_notes.create` | Create |
| `debit_notes.edit` | Edit |
| `debit_notes.void` | Void |

### Module: `routers`

| Permission Code | Action |
|-----------------|--------|
| `routers.view` | View |
| `routers.create` | Create |
| `routers.edit` | Edit |
| `routers.delete` | Delete |
| `routers.backup` | Backup |
| `routers.restore` | Restore |
| `routers.command` | Command |
| `routers.discover` | Discover |
| `routers.view_metrics` | View Metrics |

### Module: `router_groups`

| Permission Code | Action |
|-----------------|--------|
| `router_groups.view` | View |
| `router_groups.create` | Create |
| `router_groups.edit` | Edit |
| `router_groups.delete` | Delete |

### Module: `vpn`

| Permission Code | Action |
|-----------------|--------|
| `vpn.view` | View |
| `vpn.create` | Create |
| `vpn.edit` | Edit |
| `vpn.delete` | Delete |
| `vpn.rotate_keys` | Rotate Keys |
| `vpn.push_config` | Push Config |

### Module: `radius`

| Permission Code | Action |
|-----------------|--------|
| `radius.view` | View |
| `radius.sync` | Sync |
| `radius.view_accounting` | View Accounting |
| `radius.manage_nas` | Manage Nas |
| `radius.manage_profiles` | Manage Profiles |

### Module: `hotspot`

| Permission Code | Action |
|-----------------|--------|
| `hotspot.view` | View |
| `hotspot.create` | Create |
| `hotspot.edit` | Edit |
| `hotspot.delete` | Delete |
| `hotspot.users` | Users |
| `hotspot.sessions` | Sessions |
| `hotspot.disconnect` | Disconnect |
| `hotspot.portals` | Portals |

### Module: `pppoe`

| Permission Code | Action |
|-----------------|--------|
| `pppoe.view` | View |
| `pppoe.create` | Create |
| `pppoe.edit` | Edit |
| `pppoe.delete` | Delete |
| `pppoe.suspend` | Suspend |
| `pppoe.sessions` | Sessions |
| `pppoe.disconnect` | Disconnect |
| `pppoe.pools` | Pools |

### Module: `vouchers`

| Permission Code | Action |
|-----------------|--------|
| `vouchers.view` | View |
| `vouchers.create` | Create |
| `vouchers.edit` | Edit |
| `vouchers.delete` | Delete |
| `vouchers.print` | Print |
| `vouchers.revoke` | Revoke |
| `vouchers.export` | Export |

### Module: `captive_portal`

| Permission Code | Action |
|-----------------|--------|
| `captive_portal.view` | View |
| `captive_portal.create` | Create |
| `captive_portal.edit` | Edit |
| `captive_portal.delete` | Delete |
| `captive_portal.publish` | Publish |
| `captive_portal.preview` | Preview |

### Module: `tickets`

| Permission Code | Action |
|-----------------|--------|
| `tickets.view` | View |
| `tickets.create` | Create |
| `tickets.edit` | Edit |
| `tickets.assign` | Assign |
| `tickets.close` | Close |
| `tickets.escalate` | Escalate |
| `tickets.view_internal` | View Internal |

### Module: `notifications`

| Permission Code | Action |
|-----------------|--------|
| `notifications.view` | View |
| `notifications.send` | Send |
| `notifications.templates` | Templates |
| `notifications.rules` | Rules |

### Module: `reports`

| Permission Code | Action |
|-----------------|--------|
| `reports.view` | View |
| `reports.export` | Export |
| `reports.schedule` | Schedule |
| `reports.builder` | Builder |

### Module: `settings`

| Permission Code | Action |
|-----------------|--------|
| `settings.view` | View |
| `settings.edit` | Edit |
| `settings.payments` | Payments |
| `settings.branding` | Branding |
| `settings.security` | Security |
| `settings.api` | Api |
| `settings.webhooks` | Webhooks |

### Module: `audit`

| Permission Code | Action |
|-----------------|--------|
| `audit.view` | View |
| `audit.export` | Export |

### Module: `portal`

| Permission Code | Action |
|-----------------|--------|
| `portal.login` | Login |
| `portal.register` | Register |
| `portal.dashboard` | Dashboard |
| `portal.purchase` | Purchase |
| `portal.pay` | Pay |
| `portal.tickets` | Tickets |
| `portal.profile` | Profile |

### Module: `subscriptions`

| Permission Code | Action |
|-----------------|--------|
| `subscriptions.view` | View |
| `subscriptions.create` | Create |
| `subscriptions.edit` | Edit |
| `subscriptions.cancel` | Cancel |
| `subscriptions.renew` | Renew |

### Module: `fibre`

| Permission Code | Action |
|-----------------|--------|
| `fibre.view` | View |
| `fibre.create` | Create |
| `fibre.edit` | Edit |
| `fibre.delete` | Delete |
| `fibre.install` | Install |

### Module: `imports`

| Permission Code | Action |
|-----------------|--------|
| `imports.customers` | Customers |
| `imports.routers` | Routers |
| `imports.vouchers` | Vouchers |

### Module: `exports`

| Permission Code | Action |
|-----------------|--------|
| `exports.customers` | Customers |
| `exports.invoices` | Invoices |
| `exports.payments` | Payments |
| `exports.reports` | Reports |
| `exports.vouchers` | Vouchers |

### Module: `dashboard`

| Permission Code | Action |
|-----------------|--------|
| `dashboard.view` | View |
| `dashboard.customize` | Customize |

### Module: `api`

| Permission Code | Action |
|-----------------|--------|
| `api.tokens_view` | Tokens View |
| `api.tokens_create` | Tokens Create |
| `api.tokens_revoke` | Tokens Revoke |
| `api.keys_manage` | Keys Manage |

### Module: `webhooks`

| Permission Code | Action |
|-----------------|--------|
| `webhooks.view` | View |
| `webhooks.create` | Create |
| `webhooks.edit` | Edit |
| `webhooks.delete` | Delete |
| `webhooks.test` | Test |

### Module: `platform`

| Permission Code | Action |
|-----------------|--------|
| `platform.settings` | Settings |
| `platform.modules` | Modules |
| `platform.gateways` | Gateways |
| `platform.maintenance` | Maintenance |

---

## 4. Role-Permission Matrix

Legend: **Y** = granted, **—** = denied, **P** = partial (view only)

### Summary by Module

| Role | tenants | users | roles | customers | customer_groups | packages | billing | invoices | payments | credit_notes | debit_notes | routers |
|------|---|---|---|---|---|---|---|---|---|---|---|---|
| **Super Admin** **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y** |
| **ISP Admin** —  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y**  **Y** |
| **Support Agent** —  1/7  —  **Y**  1/4  1/5  1/6  1/6  1/8  —  —  1/9 |
| **Finance User** —  —  —  2/9  —  1/5  **Y**  **Y**  **Y**  **Y**  **Y**  — |
| **Technician** —  —  —  1/9  —  1/5  —  —  —  —  —  **Y** |
| **Customer** —  —  —  —  —  —  —  —  —  —  —  — |

### Detailed: ISP Admin

| Permission | Granted |
|------------|---------|
| `api.keys_manage` | Y |
| `api.tokens_create` | Y |
| `api.tokens_revoke` | Y |
| `api.tokens_view` | Y |
| `audit.export` | Y |
| `audit.view` | Y |
| `billing.create` | Y |
| `billing.edit` | Y |
| `billing.export` | Y |
| `billing.send` | Y |
| `billing.view` | Y |
| `billing.void` | Y |
| `captive_portal.create` | Y |
| `captive_portal.delete` | Y |
| `captive_portal.edit` | Y |
| `captive_portal.preview` | Y |
| `captive_portal.publish` | Y |
| `captive_portal.view` | Y |
| `credit_notes.create` | Y |
| `credit_notes.edit` | Y |
| `credit_notes.view` | Y |
| `credit_notes.void` | Y |
| `customer_groups.create` | Y |
| `customer_groups.delete` | Y |
| `customer_groups.edit` | Y |
| `customer_groups.view` | Y |
| `customers.create` | Y |
| `customers.delete` | Y |
| `customers.edit` | Y |
| `customers.export` | Y |
| `customers.import` | Y |
| `customers.kyc_verify` | Y |
| `customers.suspend` | Y |
| `customers.view` | Y |
| `customers.view_documents` | Y |
| `dashboard.customize` | Y |
| `dashboard.view` | Y |
| `debit_notes.create` | Y |
| `debit_notes.edit` | Y |
| `debit_notes.view` | Y |
| `debit_notes.void` | Y |
| `exports.customers` | Y |
| `exports.invoices` | Y |
| `exports.payments` | Y |
| `exports.reports` | Y |
| `exports.vouchers` | Y |
| `fibre.create` | Y |
| `fibre.delete` | Y |
| `fibre.edit` | Y |
| `fibre.install` | Y |
| `fibre.view` | Y |
| `hotspot.create` | Y |
| `hotspot.delete` | Y |
| `hotspot.disconnect` | Y |
| `hotspot.edit` | Y |
| `hotspot.portals` | Y |
| `hotspot.sessions` | Y |
| `hotspot.users` | Y |
| `hotspot.view` | Y |
| `imports.customers` | Y |
| `imports.routers` | Y |
| `imports.vouchers` | Y |
| `invoices.create` | Y |
| `invoices.download` | Y |
| `invoices.edit` | Y |
| `invoices.send` | Y |
| `invoices.view` | Y |
| `invoices.void` | Y |
| `notifications.rules` | Y |
| `notifications.send` | Y |
| `notifications.templates` | Y |
| `notifications.view` | Y |
| `packages.create` | Y |
| `packages.delete` | Y |
| `packages.edit` | Y |
| `packages.publish` | Y |
| `packages.view` | Y |
| `payments.allocate` | Y |
| `payments.create` | Y |
| `payments.edit` | Y |
| `payments.export` | Y |
| `payments.reconcile` | Y |
| `payments.refund` | Y |
| `payments.reverse` | Y |
| `payments.view` | Y |
| `platform.gateways` | Y |
| `platform.maintenance` | Y |
| `platform.modules` | Y |
| `platform.settings` | Y |
| `portal.dashboard` | Y |
| `portal.login` | Y |
| `portal.pay` | Y |
| `portal.profile` | Y |
| `portal.purchase` | Y |
| `portal.register` | Y |
| `portal.tickets` | Y |
| `pppoe.create` | Y |
| `pppoe.delete` | Y |
| `pppoe.disconnect` | Y |
| `pppoe.edit` | Y |
| `pppoe.pools` | Y |
| `pppoe.sessions` | Y |
| `pppoe.suspend` | Y |
| `pppoe.view` | Y |
| `radius.manage_nas` | Y |
| `radius.manage_profiles` | Y |
| `radius.sync` | Y |
| `radius.view` | Y |
| `radius.view_accounting` | Y |
| `reports.builder` | Y |
| `reports.export` | Y |
| `reports.schedule` | Y |
| `reports.view` | Y |
| `roles.assign_permissions` | Y |
| `roles.create` | Y |
| `roles.delete` | Y |
| `roles.edit` | Y |
| `roles.view` | Y |
| `router_groups.create` | Y |
| `router_groups.delete` | Y |
| `router_groups.edit` | Y |
| `router_groups.view` | Y |
| `routers.backup` | Y |
| `routers.command` | Y |
| `routers.create` | Y |
| `routers.delete` | Y |
| `routers.discover` | Y |
| `routers.edit` | Y |
| `routers.restore` | Y |
| `routers.view` | Y |
| `routers.view_metrics` | Y |
| `settings.api` | Y |
| `settings.branding` | Y |
| `settings.edit` | Y |
| `settings.payments` | Y |
| `settings.security` | Y |
| `settings.view` | Y |
| `settings.webhooks` | Y |
| `subscriptions.cancel` | Y |
| `subscriptions.create` | Y |
| `subscriptions.edit` | Y |
| `subscriptions.renew` | Y |
| `subscriptions.view` | Y |
| `tickets.assign` | Y |
| `tickets.close` | Y |
| `tickets.create` | Y |
| `tickets.edit` | Y |
| `tickets.escalate` | Y |
| `tickets.view` | Y |
| `tickets.view_internal` | Y |
| `users.assign_roles` | Y |
| `users.create` | Y |
| `users.delete` | Y |
| `users.edit` | Y |
| `users.reset_password` | Y |
| `users.view` | Y |
| `users.view_audit` | Y |
| `vouchers.create` | Y |
| `vouchers.delete` | Y |
| `vouchers.edit` | Y |
| `vouchers.export` | Y |
| `vouchers.print` | Y |
| `vouchers.revoke` | Y |
| `vouchers.view` | Y |
| `vpn.create` | Y |
| `vpn.delete` | Y |
| `vpn.edit` | Y |
| `vpn.push_config` | Y |
| `vpn.rotate_keys` | Y |
| `vpn.view` | Y |
| `webhooks.create` | Y |
| `webhooks.delete` | Y |
| `webhooks.edit` | Y |
| `webhooks.test` | Y |
| `webhooks.view` | Y |

### Detailed: Support Agent

| Permission | Granted |
|------------|---------|
| `audit.view` | Y |
| `billing.view` | Y |
| `customer_groups.view` | Y |
| `customers.create` | Y |
| `customers.delete` | Y |
| `customers.edit` | Y |
| `customers.export` | Y |
| `customers.import` | Y |
| `customers.kyc_verify` | Y |
| `customers.suspend` | Y |
| `customers.view` | Y |
| `customers.view_documents` | Y |
| `hotspot.sessions` | Y |
| `hotspot.view` | Y |
| `invoices.view` | Y |
| `notifications.send` | Y |
| `notifications.view` | Y |
| `packages.view` | Y |
| `payments.view` | Y |
| `portal.dashboard` | Y |
| `portal.login` | Y |
| `portal.pay` | Y |
| `portal.profile` | Y |
| `portal.purchase` | Y |
| `portal.register` | Y |
| `portal.tickets` | Y |
| `pppoe.sessions` | Y |
| `pppoe.view` | Y |
| `reports.view` | Y |
| `routers.view` | Y |
| `settings.view` | Y |
| `tickets.assign` | Y |
| `tickets.close` | Y |
| `tickets.create` | Y |
| `tickets.edit` | Y |
| `tickets.escalate` | Y |
| `tickets.view` | Y |
| `tickets.view_internal` | Y |
| `users.view` | Y |
| `vouchers.view` | Y |

### Detailed: Finance User

| Permission | Granted |
|------------|---------|
| `billing.create` | Y |
| `billing.edit` | Y |
| `billing.export` | Y |
| `billing.send` | Y |
| `billing.view` | Y |
| `billing.void` | Y |
| `credit_notes.create` | Y |
| `credit_notes.edit` | Y |
| `credit_notes.view` | Y |
| `credit_notes.void` | Y |
| `customers.export` | Y |
| `customers.view` | Y |
| `debit_notes.create` | Y |
| `debit_notes.edit` | Y |
| `debit_notes.view` | Y |
| `debit_notes.void` | Y |
| `invoices.create` | Y |
| `invoices.download` | Y |
| `invoices.edit` | Y |
| `invoices.send` | Y |
| `invoices.view` | Y |
| `invoices.void` | Y |
| `notifications.view` | Y |
| `packages.view` | Y |
| `payments.allocate` | Y |
| `payments.create` | Y |
| `payments.edit` | Y |
| `payments.export` | Y |
| `payments.reconcile` | Y |
| `payments.refund` | Y |
| `payments.reverse` | Y |
| `payments.view` | Y |
| `reports.export` | Y |
| `reports.view` | Y |
| `settings.payments` | Y |
| `settings.view` | Y |
| `tickets.create` | Y |
| `tickets.view` | Y |
| `vouchers.view` | Y |

### Detailed: Technician

| Permission | Granted |
|------------|---------|
| `captive_portal.create` | Y |
| `captive_portal.delete` | Y |
| `captive_portal.edit` | Y |
| `captive_portal.preview` | Y |
| `captive_portal.publish` | Y |
| `captive_portal.view` | Y |
| `customers.view` | Y |
| `hotspot.create` | Y |
| `hotspot.delete` | Y |
| `hotspot.disconnect` | Y |
| `hotspot.edit` | Y |
| `hotspot.portals` | Y |
| `hotspot.sessions` | Y |
| `hotspot.users` | Y |
| `hotspot.view` | Y |
| `packages.view` | Y |
| `pppoe.create` | Y |
| `pppoe.delete` | Y |
| `pppoe.disconnect` | Y |
| `pppoe.edit` | Y |
| `pppoe.pools` | Y |
| `pppoe.sessions` | Y |
| `pppoe.suspend` | Y |
| `pppoe.view` | Y |
| `radius.manage_nas` | Y |
| `radius.manage_profiles` | Y |
| `radius.sync` | Y |
| `radius.view` | Y |
| `radius.view_accounting` | Y |
| `reports.view` | Y |
| `router_groups.create` | Y |
| `router_groups.delete` | Y |
| `router_groups.edit` | Y |
| `router_groups.view` | Y |
| `routers.backup` | Y |
| `routers.command` | Y |
| `routers.create` | Y |
| `routers.delete` | Y |
| `routers.discover` | Y |
| `routers.edit` | Y |
| `routers.restore` | Y |
| `routers.view` | Y |
| `routers.view_metrics` | Y |
| `service_locations.view` | Y |
| `settings.network` | Y |
| `settings.view` | Y |
| `tickets.create` | Y |
| `tickets.edit` | Y |
| `tickets.view` | Y |
| `vouchers.view` | Y |
| `vpn.create` | Y |
| `vpn.delete` | Y |
| `vpn.edit` | Y |
| `vpn.push_config` | Y |
| `vpn.rotate_keys` | Y |
| `vpn.view` | Y |

---

## 5. Core Functions

### 5.1 `permission_check(string $code): bool`

**File:** `includes/auth.php`

```php
/**
 * Check if current user has a specific permission.
 * Super Admin bypasses all checks.
 * Permissions loaded at login into $_SESSION['permissions'].
 */
function permission_check(string $code): bool {
    if (!isLoggedIn()) {
        return false;
    }
    // Super Admin — platform role, tenant_id NULL
    if (role_check('super_admin')) {
        return true;
    }
    $permissions = $_SESSION['permissions'] ?? [];
    return in_array($code, $permissions, true);
}

/**
 * Check any of multiple permissions (OR logic).
 */
function permission_check_any(array $codes): bool {
    foreach ($codes as $code) {
        if (permission_check($code)) {
            return true;
        }
    }
    return false;
}
```

### 5.2 `role_check(string $role_code): bool`

```php
/**
 * Check if current user has a specific role.
 * Uses $_SESSION['roles'] array loaded at login.
 */
function role_check(string $role_code): bool {
    if (!isLoggedIn()) {
        return false;
    }
    $roles = $_SESSION['roles'] ?? [];
    return in_array($role_code, $roles, true);
}
```

### 5.3 `tenant_context(): ?int`

**File:** `includes/tenant.php`

```php
/**
 * Return active tenant_id for current request.
 * Resolution order:
 *   1. Super Admin impersonation: $_SESSION['impersonate_tenant_id']
 *   2. Staff user: $_SESSION['tenant_id']
 *   3. Domain resolution: tenant_domains lookup (fallback)
 * Returns NULL only for Super Admin without impersonation (platform queries).
 */
function tenant_context(): ?int {
    if (!empty($_SESSION['impersonate_tenant_id'])) {
        return (int) $_SESSION['impersonate_tenant_id'];
    }
    if (!empty($_SESSION['tenant_id'])) {
        return (int) $_SESSION['tenant_id'];
    }
    return null;
}

/**
 * Enforce tenant context — die if missing on tenant-scoped pages.
 */
function require_tenant_context(): int {
    $tenant_id = tenant_context();
    if ($tenant_id === null && !role_check('super_admin')) {
        http_response_code(403);
        exit('Tenant context required');
    }
    return $tenant_id ?? 0;
}
```

### 5.4 `audit_log(...)`

**File:** `includes/audit.php`

```php
/**
 * Write immutable audit log entry.
 *
 * @param string $action     e.g. 'customer.created', 'payment.refunded'
 * @param string $entity_type e.g. 'customers', 'invoices'
 * @param int|null $entity_id
 * @param array|null $old_values
 * @param array|null $new_values
 */
function audit_log(
    string $action,
    string $entity_type,
    ?int $entity_id = null,
    ?array $old_values = null,
    ?array $new_values = null
): void {
    global $pdo;
    $stmt = $pdo->prepare(
        'INSERT INTO audit_logs (tenant_id, user_id, action, entity_type, entity_id,
         old_values, new_values, ip_address, user_agent) 
         VALUES (:tenant_id, :user_id, :action, :entity_type, :entity_id,
         :old_values, :new_values, :ip, :ua)'
    );
    $stmt->execute([
        ':tenant_id' => tenant_context(),
        ':user_id' => getCurrentUserId(),
        ':action' => $action,
        ':entity_type' => $entity_type,
        ':entity_id' => $entity_id,
        ':old_values' => $old_values ? json_encode($old_values) : null,
        ':new_values' => $new_values ? json_encode($new_values) : null,
        ':ip' => $_SERVER['REMOTE_ADDR'] ?? null,
        ':ua' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 500),
    ]);
}
```

**Actions requiring audit_log (mandatory):**

| Category | Actions |
|----------|---------|
| Auth | login.success, login.failed, logout, password.changed |
| Tenants | tenant.created, tenant.suspended, tenant.impersonate |
| Customers | customer.created, customer.updated, customer.suspended, customer.deleted |
| Billing | invoice.issued, invoice.voided, payment.recorded, payment.refunded |
| Network | router.created, router.command, router.backup, vpn.peer.created |
| Security | user.created, role.assigned, permission.changed, api_token.created |

---

## 6. Session Lifecycle

### Staff Session

| Parameter | Value |
|-----------|-------|
| Session name | `ISP360SESSID` |
| Storage | PHP file sessions or Redis |
| Idle timeout | 60 minutes |
| Absolute timeout | 12 hours |
| Regenerate ID | On login, privilege change |
| Cookie flags | HttpOnly, Secure, SameSite=Strict |

### Session Data Structure

```php
$_SESSION = [
    'user_id'       => 123,
    'tenant_id'     => 5,           // NULL for super_admin
    'roles'         => ['isp_admin'],
    'permissions'   => ['customers.view', 'customers.create', ...],
    'user_data'     => ['username', 'email', 'first_name', ...],
    'session_token' => '...',       // matches sessions.session_token_hash
    'login_at'      => '2026-06-13 10:00:00',
    'last_activity' => timestamp,
    'impersonate_tenant_id' => null, // Super Admin only
];
```

### Portal Session

| Parameter | Value |
|-----------|-------|
| Session name | `ISP360PORTAL` |
| Idle timeout | 30 minutes |
| Absolute timeout | 24 hours |

```php
$_SESSION['portal'] = [
    'customer_id' => 456,
    'tenant_id'   => 5,
    'session_token' => '...',
];
```

---

## 7. Login & Brute-Force Policy

| Rule | Value |
|------|-------|
| Max failed attempts | 5 per username per 15 minutes |
| Lockout duration | 30 minutes (users.locked_until) |
| IP rate limit | 20 attempts per IP per 15 minutes |
| Log all attempts | `login_history` table |
| MFA | Optional per user (users.mfa_enabled) |
| Password policy | Min 8 chars, 1 upper, 1 lower, 1 digit |

### Login Flow

```mermaid
sequenceDiagram
    participant U as User
    participant L as login_process.php
    participant DB as MySQL
    participant S as Session
    U->>L: POST username + password
    L->>DB: Check login_history rate limit
    L->>DB: SELECT user WHERE tenant_id + username
    alt Invalid credentials
        L->>DB: INSERT login_history failed
        L-->>U: Redirect login.php?error=1
    else Account locked
        L-->>U: Redirect login.php?locked=1
    else MFA required
        L-->>U: Redirect mfa_verify.php
    else Success
        L->>DB: Load roles + permissions
        L->>DB: INSERT sessions + login_history success
        L->>S: Populate session
        L-->>U: Redirect dashboard.php
    end
```

---

## 8. API Token Scopes

| Token Type | Issued To | Scopes | Expiry |
|------------|-----------|--------|--------|
| `user` | Staff API access | Per role permissions | 90 days |
| `service` | Internal cron/integrations | Limited: radius.sync, payments.callback | 1 year |
| `router` | MikroTik polling agent | routers.status, routers.command | No expiry |
| `integration` | Third-party webhooks | Configured per tenant_api_keys | Per key |

### Token Validation (`includes/ApiAuth.php`)

```php
function validate_api_token(): ?array {
    $header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
    if (!preg_match('/Bearer\s+(\S+)/', $header, $m)) {
        return null;
    }
    $hash = hash('sha256', $m[1]);
    // Lookup api_tokens WHERE token_hash = :hash AND is_active = 1 AND expires_at > NOW()
    // Return token row with scopes JSON
}
```

---

## 9. Super Admin vs Tenant Isolation

| Capability | Super Admin | ISP Admin |
|------------|-------------|-----------|
| View all tenants | Yes | No |
| Create tenants | Yes | No |
| Impersonate tenant | Yes (`impersonate_tenant`) | No |
| Cross-tenant queries | Explicit `tenant_id` param | Blocked |
| Platform settings | Yes | No |
| Tenant billing (SaaS) | Yes | No |
| tenant_id on user | NULL | Required |

### Impersonation Rules

1. Only `super_admin` may call `ajax/impersonate_tenant.php`
2. Sets `$_SESSION['impersonate_tenant_id']` — does NOT change `user_id`
3. Banner displayed: "Viewing as {tenant_name}" with End Impersonation button
4. All actions audited with `tenant.impersonate` + real user_id
5. `ajax/end_impersonation.php` clears impersonate flag

---

## 10. Page Access Map

Every root `.php` page maps to required permission(s). Enforced at top of each file.

| Page | Permission(s) | Roles |
|------|---------------|-------|
| `airtel_transactions.php` | `payments.view` | ISP Admin, Finance |
| `api_tokens.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `audit_logs.php` | `settings.view` | ISP Admin, Support, Finance, Technician |
| `bandwidth_profiles.php` | `routers.edit` | ISP Admin, Technician |
| `billing.php` | `billing.view` | ISP Admin, Finance |
| `billing_cycles.php` | `billing.edit` | ISP Admin, Finance |
| `captive_portal_edit.php` | `hotspot.edit` | ISP Admin, Support, Finance, Technician |
| `captive_portal_preview.php` | `hotspot.view` | ISP Admin, Support, Finance, Technician |
| `captive_portal_themes.php` | `hotspot.edit` | ISP Admin, Support, Finance, Technician |
| `captive_portals.php` | `hotspot.edit` | ISP Admin, Support, Finance, Technician |
| `change_password.php` | `authenticated` | ISP Admin, Support, Finance, Technician |
| `credit_notes.php` | `billing.view` | ISP Admin, Finance |
| `customer_devices.php` | `customers.view` | ISP Admin, Support, Finance, Technician |
| `customer_documents.php` | `customers.edit` | ISP Admin, Support, Finance, Technician |
| `customer_edit.php` | `customers.create|customers.edit` | ISP Admin, Support, Finance, Technician |
| `customer_groups.php` | `customers.view` | ISP Admin, Support, Finance, Technician |
| `customer_import.php` | `customers.create` | ISP Admin, Support, Finance, Technician |
| `customer_kyc.php` | `customers.edit` | ISP Admin, Support, Finance, Technician |
| `customer_notes.php` | `customers.view` | ISP Admin, Support, Finance, Technician |
| `customer_subscriptions.php` | `customers.edit` | ISP Admin, Support, Finance, Technician |
| `customer_view.php` | `customers.view` | ISP Admin, Support, Finance, Technician |
| `customers.php` | `customers.view` | ISP Admin, Support, Finance, Technician |
| `dashboard.php` | `authenticated` | All staff |
| `debit_notes.php` | `billing.view` | ISP Admin, Finance |
| `dunning.php` | `billing.edit` | ISP Admin, Finance |
| `email_logs.php` | `settings.view` | ISP Admin, Support, Finance, Technician |
| `fibre_connections.php` | `pppoe.view` | ISP Admin, Support, Finance, Technician |
| `forgot_password.php` | `public` | ISP Admin, Support, Finance, Technician |
| `hotspot_edit.php` | `hotspot.create|hotspot.edit` | ISP Admin, Support, Finance, Technician |
| `hotspot_mac_auth.php` | `hotspot.edit` | ISP Admin, Support, Finance, Technician |
| `hotspot_portals.php` | `hotspot.edit` | ISP Admin, Support, Finance, Technician |
| `hotspot_profiles.php` | `hotspot.edit` | ISP Admin, Support, Finance, Technician |
| `hotspot_sessions.php` | `hotspot.view` | ISP Admin, Support, Finance, Technician |
| `hotspot_usage.php` | `hotspot.view` | ISP Admin, Support, Finance, Technician |
| `hotspot_users.php` | `hotspot.users` | ISP Admin, Support, Finance, Technician |
| `hotspot_walled_garden.php` | `hotspot.edit` | ISP Admin, Support, Finance, Technician |
| `hotspots.php` | `hotspot.view` | ISP Admin, Support, Finance, Technician |
| `index.php` | `public` | ISP Admin, Support, Finance, Technician |
| `invoice_edit.php` | `billing.create|billing.edit` | ISP Admin, Finance |
| `invoice_view.php` | `billing.view` | ISP Admin, Finance |
| `invoices.php` | `billing.view` | ISP Admin, Finance |
| `login.php` | `public` | ISP Admin, Support, Finance, Technician |
| `login_history.php` | `users.view` | ISP Admin, Support, Finance, Technician |
| `login_process.php` | `public` | ISP Admin, Support, Finance, Technician |
| `logout.php` | `authenticated` | ISP Admin, Support, Finance, Technician |
| `momo_transactions.php` | `payments.view` | ISP Admin, Finance |
| `mpesa_transactions.php` | `payments.view` | ISP Admin, Finance |
| `nas_devices.php` | `routers.view` | ISP Admin, Technician |
| `notification_rules.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `notification_templates.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `notifications.php` | `settings.view` | ISP Admin, Support, Finance, Technician |
| `package_edit.php` | `packages.create|packages.edit` | ISP Admin, Support, Finance, Technician |
| `package_groups.php` | `packages.view` | ISP Admin, Support, Finance, Technician |
| `package_promotions.php` | `packages.edit` | ISP Admin, Support, Finance, Technician |
| `package_view.php` | `packages.view` | ISP Admin, Support, Finance, Technician |
| `packages.php` | `packages.view` | ISP Admin, Support, Finance, Technician |
| `payment_allocate.php` | `payments.create` | ISP Admin, Finance |
| `payment_callbacks.php` | `payments.view` | ISP Admin, Finance |
| `payment_disbursements.php` | `payments.create` | ISP Admin, Finance |
| `payment_plans.php` | `payments.view` | ISP Admin, Finance |
| `payment_reconciliation.php` | `payments.reconcile` | ISP Admin, Finance |
| `payment_refunds.php` | `payments.refund` | ISP Admin, Finance |
| `payment_view.php` | `payments.view` | ISP Admin, Finance |
| `payments.php` | `payments.view` | ISP Admin, Finance |
| `permissions.php` | `users.view` | ISP Admin, Support, Finance, Technician |
| `portal_dashboard.php` | `portal_auth` | Customer |
| `portal_invoice_view.php` | `portal_auth` | Customer |
| `portal_invoices.php` | `portal_auth` | Customer |
| `portal_login.php` | `public` | Customer |
| `portal_logout.php` | `portal_auth` | Customer |
| `portal_packages.php` | `portal_auth` | Customer |
| `portal_password.php` | `portal_auth` | Customer |
| `portal_pay.php` | `portal_auth` | Customer |
| `portal_payments.php` | `portal_auth` | Customer |
| `portal_profile.php` | `portal_auth` | Customer |
| `portal_purchase.php` | `portal_auth` | Customer |
| `portal_register.php` | `public` | Customer |
| `portal_ticket_view.php` | `portal_auth` | Customer |
| `portal_tickets.php` | `portal_auth` | Customer |
| `portal_usage.php` | `portal_auth` | Customer |
| `portal_voucher.php` | `portal_auth` | Customer |
| `pppoe_account_edit.php` | `pppoe.create|pppoe.edit` | ISP Admin, Support, Finance, Technician |
| `pppoe_accounts.php` | `pppoe.view` | ISP Admin, Support, Finance, Technician |
| `pppoe_ip_pools.php` | `pppoe.edit` | ISP Admin, Support, Finance, Technician |
| `pppoe_profiles.php` | `pppoe.view` | ISP Admin, Support, Finance, Technician |
| `pppoe_sessions.php` | `pppoe.view` | ISP Admin, Support, Finance, Technician |
| `pppoe_static_ips.php` | `pppoe.edit` | ISP Admin, Support, Finance, Technician |
| `profile.php` | `authenticated` | ISP Admin, Support, Finance, Technician |
| `radius_accounting.php` | `pppoe.view` | ISP Admin, Technician |
| `radius_groups.php` | `pppoe.view` | ISP Admin, Technician |
| `radius_profiles.php` | `pppoe.view` | ISP Admin, Technician |
| `radius_sync.php` | `routers.view` | ISP Admin, Technician |
| `report_aging.php` | `reports.view` | ISP Admin, Finance |
| `report_builder.php` | `reports.export` | ISP Admin, Finance |
| `report_collections.php` | `reports.view` | ISP Admin, Finance |
| `report_customers.php` | `reports.view` | ISP Admin, Finance |
| `report_hotspot.php` | `reports.view` | ISP Admin, Finance |
| `report_network.php` | `reports.view` | ISP Admin, Finance |
| `report_pppoe.php` | `reports.view` | ISP Admin, Finance |
| `report_revenue.php` | `reports.view` | ISP Admin, Finance |
| `report_routers.php` | `reports.view` | ISP Admin, Finance |
| `report_schedules.php` | `reports.export` | ISP Admin, Finance |
| `report_subscriptions.php` | `reports.view` | ISP Admin, Finance |
| `report_tax.php` | `reports.view` | ISP Admin, Finance |
| `report_tickets.php` | `reports.view` | ISP Admin, Finance |
| `report_usage.php` | `reports.view` | ISP Admin, Finance |
| `report_vouchers.php` | `reports.view` | ISP Admin, Finance |
| `reset_password.php` | `public` | ISP Admin, Support, Finance, Technician |
| `reset_password_page.php` | `public` | ISP Admin, Support, Finance, Technician |
| `role_edit.php` | `users.edit` | ISP Admin, Support, Finance, Technician |
| `roles.php` | `users.view` | ISP Admin, Support, Finance, Technician |
| `router_alerts.php` | `routers.view` | ISP Admin, Technician |
| `router_backup.php` | `routers.backup` | ISP Admin, Technician |
| `router_commands.php` | `routers.command` | ISP Admin, Technician |
| `router_discovery.php` | `routers.create` | ISP Admin, Technician |
| `router_edit.php` | `routers.create|routers.edit` | ISP Admin, Technician |
| `router_firewall.php` | `routers.edit` | ISP Admin, Technician |
| `router_groups.php` | `routers.view` | ISP Admin, Technician |
| `router_metrics.php` | `routers.view` | ISP Admin, Technician |
| `router_queues.php` | `routers.edit` | ISP Admin, Technician |
| `router_schedules.php` | `routers.edit` | ISP Admin, Technician |
| `router_scripts.php` | `routers.edit` | ISP Admin, Technician |
| `router_view.php` | `routers.view` | ISP Admin, Technician |
| `routers.php` | `routers.view` | ISP Admin, Technician |
| `service_locations.php` | `routers.view` | ISP Admin, Technician |
| `settings_api.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_billing.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_branding.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_general.php` | `settings.view` | ISP Admin, Support, Finance, Technician |
| `settings_localization.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_modules.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_network.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_notifications.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_payments.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_radius.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_security.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `settings_webhooks.php` | `settings.edit` | ISP Admin, Support, Finance, Technician |
| `sms_logs.php` | `settings.view` | ISP Admin, Support, Finance, Technician |
| `tenant_billing.php` | `tenants.edit` | Super Admin |
| `tenant_domains.php` | `tenants.edit` | Super Admin |
| `tenant_edit.php` | `tenants.create|tenants.edit` | Super Admin |
| `tenant_modules.php` | `tenants.edit` | Super Admin |
| `tenant_view.php` | `tenants.view` | Super Admin |
| `tenants.php` | `tenants.view` | Super Admin |
| `ticket_categories.php` | `tickets.view` | ISP Admin, Support |
| `ticket_edit.php` | `tickets.create` | ISP Admin, Support |
| `ticket_sla.php` | `settings.edit` | ISP Admin, Support |
| `ticket_view.php` | `tickets.view` | ISP Admin, Support |
| `tickets.php` | `tickets.view` | ISP Admin, Support |
| `user_edit.php` | `users.create|users.edit` | ISP Admin, Support, Finance, Technician |
| `users.php` | `users.view` | ISP Admin, Support, Finance, Technician |
| `voucher_batch_edit.php` | `vouchers.create` | ISP Admin, Support, Finance, Technician |
| `voucher_batch_view.php` | `vouchers.view` | ISP Admin, Support, Finance, Technician |
| `voucher_batches.php` | `vouchers.view` | ISP Admin, Support, Finance, Technician |
| `voucher_print.php` | `vouchers.print` | ISP Admin, Support, Finance, Technician |
| `voucher_resellers.php` | `vouchers.view` | ISP Admin, Support, Finance, Technician |
| `voucher_templates.php` | `vouchers.create` | ISP Admin, Support, Finance, Technician |
| `vouchers.php` | `vouchers.view` | ISP Admin, Support, Finance, Technician |
| `vpn_logs.php` | `vpn.view` | ISP Admin, Technician |
| `vpn_peer_edit.php` | `vpn.edit` | ISP Admin, Technician |
| `vpn_peers.php` | `vpn.view` | ISP Admin, Technician |
| `vpn_server_edit.php` | `vpn.create|vpn.edit` | ISP Admin, Technician |
| `vpn_servers.php` | `vpn.view` | ISP Admin, Technician |
| `vpn_tunnels.php` | `vpn.view` | ISP Admin, Technician |

### Public Pages (no permission_check)

| Page | Access |
|------|--------|
| `login.php` | Public; redirect if logged in |
| `forgot_password.php` | Public |
| `reset_password_page.php` | Public with valid token |
| `portal_login.php` | Public; redirect if portal session |
| `portal_register.php` | Public if tenant allows self-registration |

### Handler Pages (no HTML)

| Page | Behavior |
|------|----------|
| `index.php` | Redirect login or dashboard |
| `login_process.php` | Authenticate + redirect |
| `logout.php` | Destroy session + redirect login |
| `reset_password.php` | Process token + redirect |
| `portal_logout.php` | Destroy portal session |

---

## 11. Menu Configuration

**File:** `includes/menu_config.php`

Menu is filtered at render time by `permission_check()` on each item's `permission` key.

```php
return [
    ['type' => 'link', 'label' => 'Dashboard', 'url' => 'dashboard.php',
     'icon' => 'bi-speedometer2', 'permission' => null], // requireLogin only
    ['type' => 'header', 'label' => 'CRM'],
    ['type' => 'link', 'label' => 'Customers', 'url' => 'customers.php',
     'icon' => 'bi-people', 'permission' => 'customers.view'],
    ['type' => 'dropdown', 'label' => 'Billing', 'icon' => 'bi-receipt',
     'permission' => 'billing.view',
     'items' => [
         ['label' => 'Invoices', 'url' => 'invoices.php', 'permission' => 'billing.view'],
         ['label' => 'Payments', 'url' => 'payments.php', 'permission' => 'payments.view'],
     ]],
    ['type' => 'header', 'label' => 'Network'],
    ['type' => 'link', 'label' => 'Routers', 'url' => 'routers.php',
     'icon' => 'bi-router', 'permission' => 'routers.view'],
    // ... full menu in implementation
];
```

### Menu Filter Function

```php
function getFilteredMenu(): array {
    $menu = require __DIR__ . '/menu_config.php';
    return array_filter($menu, function ($item) {
        if (empty($item['permission'])) {
            return isLoggedIn();
        }
        if (isset($item['items'])) {
            $item['items'] = array_filter($item['items'], fn($s) => permission_check($s['permission']));
            return count($item['items']) > 0;
        }
        return permission_check($item['permission']);
    });
}
```

---

## 12. AJAX & API Permission Enforcement

Every `/ajax/*.php` endpoint must:

1. `require_once('includes/config.php')` + `includes/auth.php`
2. Call `permission_check()` matching the action
3. Call `require_tenant_context()` for tenant-scoped data
4. Return JSON `{success: false, error: 'Access denied'}` with HTTP 403

| AJAX Pattern | Permission |
|--------------|------------|
| `get_*.php` | `{module}.view` |
| `save_*.php` | `{module}.create` or `{module}.edit` |
| `delete_*.php` | `{module}.delete` |
| `export_*.php` | `{module}.export` or `reports.export` |
| `portal_*.php` | Portal session valid (no staff permission) |

---

## 13. Database Tables

| Table | RBAC Purpose |
|-------|--------------|
| `roles` | Role definitions (platform + tenant) |
| `permissions` | Global permission catalog |
| `role_permissions` | Role-to-permission mapping |
| `users` | User accounts |
| `user_roles` | User-to-role assignment (supports expiry) |
| `sessions` | Active staff sessions |
| `portal_sessions` | Customer portal sessions |
| `api_tokens` | API bearer tokens with scopes |
| `login_history` | Login attempt audit |
| `audit_logs` | Action audit trail |
| `ip_whitelist` | Optional IP restriction per user/tenant |
| `security_events` | Brute-force, anomaly detection |

---

## 14. Implementation Checklist

- [ ] Rewrite `includes/auth.php` — PDO, permission_check, role_check
- [ ] Create `includes/tenant.php` — tenant_context, require_tenant_context
- [ ] Create `includes/audit.php` — audit_log wrapper
- [ ] Create `includes/ApiAuth.php` — token validation
- [ ] Create `includes/menu_config.php` — full menu with permissions
- [ ] Seed `role_permissions` for all 6 default roles
- [ ] Add `access_denied.php` root page
- [ ] Login flow: load permissions into session at login_process.php

## Change Log

| Version | Date | Change |
|---------|------|--------|
| 1.0 | 2026-06-13 | Initial RBAC spec — 182 permissions, 6 roles, 164 page mappings |
