PyroRadius — User Roles & Access Control¶
Version: 1.0 (Documentation) Last Updated: 2026-07-13
Table of Contents¶
- Role Hierarchy Overview
- Super Admin
- Admin
- Dealer
- Sub Dealer
- Customer Portal User
- Field Engineer
- Dealer Hierarchy Deep Dive
- View As Feature
- Permission System Architecture
- Permission Keys Reference
- Customer Portal Details
Role Hierarchy Overview¶
┌─────────────────────────────────────────────────────────────────────┐
│ SUPER ADMIN │
│ Full access to everything. System settings, all dealers, │
│ all customers, all financial data, RADIUS config. │
│ Role string: 'super_admin' │
└───────────────────────────────┬─────────────────────────────────────┘
│ manages
┌───────────────────────────────▼─────────────────────────────────────┐
│ ADMIN │
│ Operational staff. Manages dealers, packages, NAS devices. │
│ Can "View As" any dealer. Cannot access system-level settings. │
│ Role string: 'admin' │
└───────────────────────────────┬─────────────────────────────────────┘
│ manages
┌───────────────────────────────▼─────────────────────────────────────┐
│ DEALER │
│ Independent reseller. Sees only their own customers. │
│ Permissions explicitly granted as JSONB keys. │
│ Role string: 'dealer' │
└───────────────────────────────┬─────────────────────────────────────┘
│ manages
┌───────────────────────────────▼─────────────────────────────────────┐
│ SUB DEALER │
│ Under a dealer. Restricted subset of dealer permissions. │
│ Cannot see other sub-dealers' customers. │
│ Role string: 'sub_dealer' │
└─────────────────────────────────────────────────────────────────────┘
Separate authentication system:
┌─────────────────────────────────────────────────────────────────────┐
│ CUSTOMER PORTAL USER │
│ End customer. Logs in at /portal. Sees only their own account. │
│ No role string — identified by customer_id in portal session. │
└─────────────────────────────────────────────────────────────────────┘
Super Admin¶
Role string: super_admin
Defined in: users.role column
Permission bypass: Yes — super_admin bypasses all JSONB permission checks
What Super Admin Can See and Do¶
| Area | Access |
|---|---|
| All customers (all dealers) | Full CRUD |
| All dealers and sub-dealers | Full CRUD |
| All invoices and payments (system-wide) | Full CRUD |
| All ledger entries | Read + adjustment |
| Package management | Full CRUD |
| NAS device management | Full CRUD + API credential management |
| OLT management | Full CRUD |
| WhatsApp templates | Full CRUD |
| Campaign management | Full CRUD |
| System settings (key-value store) | Read + Write |
| Company profile | Read + Write |
| Audit logs | Read + Prune |
| Cron job scheduler | Full CRUD + manual trigger |
| RADIUS direct management | Full access |
| TR-069 / GenieACS | Full access |
| Report export (all data) | Full access |
| Bulk customer import | Full access |
| User management (all roles) | Full CRUD |
| View As any dealer | Yes |
| Firewall / website bypass | Full CRUD |
| Engineer team management | Full CRUD |
What Super Admin Cannot Do (by policy, not technical)¶
- Directly execute shell commands (no such feature in the UI)
- Delete
audit_logsrecords individually (only bulk pruning via command) - Permanently delete paid invoices (soft delete or archive only)
Admin¶
Role string: admin
Defined in: users.role column
Permission bypass: Yes — admin bypasses all JSONB permission checks
What Admin Can See and Do¶
Admins have near-identical access to super_admin with these distinctions:
| Area | Access |
|---|---|
| All customers (all dealers) | Full CRUD |
| Dealers | Create + Edit (cannot delete dealers with customers) |
| System settings | Read only (some keys) |
| Company profile | Read only |
| Billing reports (system-wide) | Yes |
| View As any dealer | Yes |
| Package management | Full CRUD |
| NAS management | Full CRUD |
| OLT management | Full CRUD |
| Audit logs | Read |
| Cron jobs | Full management |
| User management | Create/Edit dealers and sub-dealers |
What Admin Cannot Do¶
- Access super_admin-only settings (if any are gated at that level)
- Manage other admin accounts (only super_admin manages admins)
Dealer¶
Role string: dealer
Defined in: users.role column
Permission bypass: No — all access controlled by JSONB permissions column
Scoping: All queries automatically filtered by dealer_id = auth()->id()
What Dealer Can See (Always)¶
Even a dealer with minimal permissions always sees: - Their own profile - Their own financial summary (what they owe PyroNet) - Customers assigned to them (list view, basic info) - Their own sub-dealers
What Dealer Can Do (Depends on Granted Permissions)¶
The following capabilities are controlled by explicit permission keys granted by an admin:
| Permission Key | What It Enables |
|---|---|
customers.view |
View full customer details (not just list) |
customers.create |
Add new customers |
customers.edit |
Edit customer details, change package, change status |
customers.delete |
Soft-delete customers |
billing.invoices |
View invoices |
billing.payments |
Record payments |
billing.recharge |
Renew customer subscriptions |
reports.view |
Access reports module |
whatsapp.send |
Manually send WhatsApp messages |
tickets.view |
View support tickets |
tickets.create |
Create tickets |
tickets.manage |
Assign and resolve tickets |
What Dealer Cannot Do (Regardless of Permissions)¶
- See other dealers' customers (hard database scoping)
- Access system settings
- Manage packages or NAS devices
- View audit logs of other dealers
- Access RADIUS configuration
- Use the "View As" feature
- Add packages (they sell only pre-assigned packages via
dealer_package)
Sub Dealer¶
Role string: sub_dealer
Defined in: users.role column
Permission bypass: No
Scoping: Filtered by both dealer_id (parent dealer) AND sub_dealer_id (own ID)
What Sub Dealer Can See¶
A sub-dealer sees only:
- Customers assigned to them specifically (customers.sub_dealer_id = auth()->id())
- Their own profile
- Their own financial summary within the dealer's account
What Sub Dealer Can Do¶
Sub-dealers receive a subset of whatever permissions their parent dealer has been granted. An admin cannot grant a sub-dealer more permissions than the parent dealer has.
Typical sub-dealer permissions: - Record payments for their own customers - View customer status and expiry - View invoices for their customers
Dealer-to-Sub-Dealer Relationship¶
users table:
id=10, role='dealer', dealer_id=NULL, name='Dealer ABC'
id=20, role='sub_dealer', dealer_id=10, name='Sub Dealer XYZ'
customers table:
customer_id=100, dealer_id=10, sub_dealer_id=20, name='Customer 1'
customer_id=101, dealer_id=10, sub_dealer_id=NULL, name='Customer 2' (dealer's own)
customer_id=102, dealer_id=10, sub_dealer_id=20, name='Customer 3'
Sub Dealer XYZ (id=20) sees: customers 100, 102 (where sub_dealer_id=20)
Dealer ABC (id=10) sees: customers 100, 101, 102 (all where dealer_id=10)
Customer Portal User¶
Authentication: Separate Laravel guard — portal guard
Route prefix: /portal
Sessions: Separate session namespace from admin sessions
Customer portal users are end-customers of Indus Broadband. They do not have a role in the users table; instead, they authenticate against the customers table (or a separate portal credentials field). Their session contains only their customer_id.
What Customer Portal Users Can Do¶
| Feature | Access |
|---|---|
| View own account details | Yes (name, package, expiry, connection status) |
| View own invoice history | Yes (download PDF) |
| View current balance | Yes |
| View payment history | Yes |
| Create support ticket | Yes |
| View own ticket status | Yes |
| View own traffic usage (if enabled) | Yes |
| Change own portal password | Yes |
What Customer Portal Users Cannot Do¶
- See any other customer's data
- Modify their own package, expiry, or connection settings
- Record payments (contact dealer or sub-dealer)
- Access any admin routes
Portal Route Separation¶
All portal routes are in routes/portal.php and use:
- Separate middleware group: portal (not web)
- Separate auth guard: portal
- Separate login controller: Auth\PortalLoginController
- Separate session: portal users never share auth tokens with admin users
Field Engineer¶
Field engineers are managed via the EngineerTeam model and the Engineer module in the admin UI. They may or may not have user accounts in the users table. The engineer module tracks:
- Engineer team members and their assignments
- Ticket assignments to specific engineers
- Installation/fault job tracking
If engineers have user accounts, their role would be a restricted admin variant or a custom role depending on implementation. The engineer_teams table links engineers to teams, and tickets can be assigned to engineers by admin/dealer staff.
Engineers cannot: - Access billing or financial data - Create/delete customers - Manage RADIUS or network configuration
Dealer Hierarchy Deep Dive¶
How Dealers Are Created¶
- Super Admin or Admin creates a dealer user account (role =
dealer) - Admin assigns permissions to the dealer (JSONB keys in
users.permissions) - Admin assigns packages to the dealer via
dealer_packagepivot (with per-dealer pricing) - Dealer can then log in and start creating customers
How Sub-Dealers Are Created¶
- A dealer (or admin) creates a sub-dealer user account under the dealer's account
users.dealer_idis set to the parent dealer'susers.idusers.role=sub_dealer- Permissions assigned as a subset of the dealer's permissions
Customer Ownership¶
A customer belongs to exactly one dealer and optionally one sub-dealer:
- customers.dealer_id = the dealer (always set)
- customers.sub_dealer_id = the sub-dealer (nullable)
When a sub-dealer creates a customer, both dealer_id and sub_dealer_id are set. When a dealer creates a customer directly, only dealer_id is set.
Financial Chain¶
Customer pays → recorded as payment with dealer_id
↓
Dealer owes PyroNet for activated customers
↓
PyroNet (super_admin/admin) views dealer-level financial summaries
↓
Reports show: dealer collection vs. dealer liability to PyroNet
The dealer_package.price column defines what PyroNet charges the dealer per customer per month for a given package. The dealer collects their own price from customers (possibly higher) and keeps the margin.
View As Feature¶
The "View As" feature allows super_admin and admin users to impersonate the view of any dealer without logging out. This is used to: - Diagnose dealer-reported issues - Verify that dealer scoping is working correctly - Audit what a specific dealer can see
How It Works¶
- Admin selects a dealer from a dropdown (typically in the admin navigation or a dedicated "View As" modal)
- The selected dealer ID is stored in the admin's session (e.g.,
session(['viewing_as_dealer' => $dealerId])) - A middleware or controller base class checks for this session key
- If set, all queries are scoped as if the authenticated user were that dealer
- A banner is shown in the UI: "Viewing as: Dealer Name [Exit View As]"
- Admin clicks "Exit View As" to clear the session key and return to their own view
Security Notes¶
- "View As" does not give admin the dealer's JSONB permissions — the admin retains their own bypass
- All actions taken while in "View As" mode are logged under the admin's own
user_idin audit_logs, with a note of the dealer being viewed as - "View As" is not available to dealers themselves
Permission System Architecture¶
Storage¶
Permissions are stored in the users.permissions JSONB column. Each key represents a capability:
{
"customers.view": true,
"customers.create": true,
"customers.edit": true,
"customers.delete": false,
"billing.invoices": true,
"billing.payments": true,
"billing.recharge": false,
"reports.view": true,
"whatsapp.send": false,
"tickets.view": true,
"tickets.create": true,
"tickets.manage": false
}
Only explicitly set keys matter. A missing key is treated the same as false.
The User::can() Method¶
Laravel's Gate / Policy system is used, but PyroRadius also has a custom can() check on the User model for JSONB-based permissions:
// In the User model or a trait:
public function hasPermission(string $permission): bool
{
// super_admin and admin bypass all checks
if (in_array($this->role, ['super_admin', 'admin'])) {
return true;
}
// For dealer and sub_dealer: check JSONB permissions
return ($this->permissions[$permission] ?? false) === true;
}
This method is called:
- In controllers before performing actions
- In policies (app/Policies/)
- In React frontend via shared auth.permissions prop
Granting and Revoking Permissions¶
Permissions are managed via the User management UI (admin side). When an admin edits a dealer's permissions:
- The form sends the full permissions array
UserController@updatevalidates and updatesusers.permissionscolumn- The JSONB column is replaced atomically
- AuditLogger records the old and new permissions values
// Programmatic grant (in code):
$dealer->permissions = array_merge($dealer->permissions ?? [], [
'billing.payments' => true,
]);
$dealer->save();
// Programmatic revoke:
$dealer->permissions = array_merge($dealer->permissions ?? [], [
'customers.delete' => false,
]);
$dealer->save();
React Frontend Permission Checks¶
Permissions are shared to the React frontend via Inertia's shared props (set in HandleInertiaRequests middleware):
// In HandleInertiaRequests.php:
'auth' => [
'user' => $request->user(),
'permissions' => $request->user()?->permissions ?? [],
'role' => $request->user()?->role,
'isAdmin' => in_array($request->user()?->role, ['super_admin', 'admin']),
]
In React components:
import { usePage } from '@inertiajs/react';
export default function CreateCustomerButton() {
const { auth } = usePage().props;
// Check permission
if (!auth.isAdmin && !auth.permissions?.['customers.create']) {
return null; // Don't render the button
}
return <button>Add Customer</button>;
}
Important: Frontend permission checks are for UX only (hiding/showing buttons). They are never the sole security gate. All security enforcement happens server-side in controllers and policies.
Permission Keys Reference¶
| Permission Key | What It Controls | Roles with Default Access |
|---|---|---|
customers.view |
View full customer detail pages | dealer (explicitly granted) |
customers.create |
Create new customers | dealer (explicitly granted) |
customers.edit |
Edit customer info, change package/status | dealer (explicitly granted) |
customers.delete |
Soft-delete customers | dealer (rarely granted) |
customers.pppoe |
View/edit PPPoE credentials | dealer (sometimes granted) |
billing.invoices |
View customer invoices | dealer (explicitly granted) |
billing.payments |
Record payment transactions | dealer (typically granted) |
billing.recharge |
Renew customer subscription (extend expiry) | dealer (explicitly granted) |
billing.ledger |
View ledger entries | dealer (explicitly granted) |
billing.adjustments |
Make manual ledger adjustments | admin only by default |
reports.view |
Access reports and analytics module | dealer (explicitly granted) |
reports.export |
Export reports to Excel | dealer (explicitly granted) |
whatsapp.send |
Manually send WhatsApp messages | dealer (sometimes granted) |
whatsapp.templates |
Manage WhatsApp templates | admin only by default |
whatsapp.campaigns |
Create and run broadcast campaigns | admin only by default |
tickets.view |
View support tickets | dealer (explicitly granted) |
tickets.create |
Create new tickets | dealer (explicitly granted) |
tickets.manage |
Assign, close, resolve tickets | dealer (sometimes granted) |
network.nas |
View NAS devices | admin only |
network.olts |
View OLT devices | admin only |
network.signal |
View ONT signal data | dealer (sometimes granted) |
import.customers |
Bulk import customers via Excel | admin only |
audit.view |
View audit logs | admin only |
system.settings |
Edit system settings | super_admin only |
system.company |
Edit company profile | super_admin only |
system.crons |
Manage scheduled jobs | super_admin / admin |
This table represents the conceptual permission keys. Verify against the actual codebase for exact key strings used in controller and policy checks.
Customer Portal Details¶
Authentication Flow¶
Customer visits: https://your-server-ip/portal/login
→ Enters: username (pppoe_username or phone) + portal password
→ Portal guard authenticates against customers table (or portal_credentials table)
→ Session created under portal guard namespace
→ Redirect to /portal/dashboard
Portal Routes¶
All portal routes are prefixed with /portal and use the portal middleware group:
/portal/login → portal login page
/portal/dashboard → account overview
/portal/invoices → invoice list
/portal/invoices/{id} → invoice detail + PDF download
/portal/payments → payment history
/portal/tickets → support tickets
/portal/tickets/create → create new ticket
/portal/profile → change portal password
Data Isolation¶
Portal users see strictly their own data. The portal guard's user object (the Customer model) is used to scope all queries:
// In portal controllers:
$customer = Auth::guard('portal')->user(); // The Customer model
$invoices = Invoice::where('customer_id', $customer->id)->get();
// No dealer scoping needed here — customer is the scope
Portal vs Admin Sessions¶
Portal sessions and admin sessions are completely separate:
- Admin: SESSION_DRIVER=redis, key prefix admin_
- Portal: separate cookie name, separate session prefix
- A user can be logged in as both an admin and a portal customer simultaneously in different browser tabs without conflict (though this would be unusual in practice)
For authorization code patterns and IDOR prevention, see 05_Authorization_Model.md. For system architecture context, see 01_System_Architecture.md.