Skip to content

Users & Permissions

ProxPanel’s authorization model has two layers: user_type (a hard-coded enum on the users table) and permissions (a flexible 220-entry catalog assigned through permission groups). user_type decides the broad role; permission groups decide what fine-grained actions someone can take.

Both layers must agree before an action is allowed. A reseller user_type with the nas.delete permission can delete NAS rows; an admin user_type bypasses permission checks entirely and can do anything.

Sidebar → Users (people-with-gear icon) and Permissions (lock icon) are two related pages.

PagePurposeURL
UsersCreate/edit user accounts (admin, reseller, support, etc.)/users
PermissionsManage permission groups and assignments/permissions

Both require their respective view permissions. In practice these are admin-only pages.

Stored in users.user_type. Six values, defined in models/user.go:

ValueCode (int)What it represents
subscriberUserTypeSubscriber (1)End-customer account for the self-service customer portal. Not a panel/admin role.
resellerUserTypeReseller (2)Owns subscribers. Scoped to own + sub-reseller subscribers unless subscribers.view_all. A sub-reseller is simply a reseller with parent_id set — same enum value, same code paths.
supportUserTypeSupport (3)Read-only operator, typically for L1 support.
adminUserTypeAdmin (4)Full system access. Bypasses all permission checks. There is always at least one admin.
collectorUserTypeCollector (5)Money-collection role — sees billing and can record payments.
readonlyUserTypeReadonly (6)View-only across the panel; cannot mutate anything.

There is no separate sub_reseller enum value — sub-resellers are reseller rows with a parent_id.

support, collector, and readonly users can be tied to a specific reseller via users.reseller_id. When tied, they only see that reseller’s data — the same scoping logic that applies to resellers (fixed in v1.0.549 to apply to all reseller-tied user types, not just reseller).

Stored in the permissions table. Each row has name (dotted, e.g. subscribers.view) and description. The full catalog is INSERT’d on first-install from schema.sql. Categories:

PrefixExamples
dashboard.*dashboard.view_admin
subscribers.*subscribers.view, subscribers.view_all, subscribers.create, subscribers.edit, subscribers.delete, subscribers.renew, subscribers.disconnect, subscribers.reset_fup, subscribers.reset_mac, subscribers.inactivate, subscribers.change_service, subscribers.add_days, subscribers.refill_quota, subscribers.rename, subscribers.ping, subscribers.view_graph, subscribers.bandwidth_rules, subscribers.change_bulk, subscribers.view_password
services.*services.view, services.create, services.edit, services.delete
nas.*nas.view, nas.create, nas.edit, nas.delete
sessions.*sessions.view, sessions.view_all
resellers.*resellers.view, resellers.create, resellers.edit, resellers.delete, resellers.impersonate
invoices.*invoices.view, invoices.create, invoices.edit, invoices.delete
prepaid.*prepaid.view, prepaid.create, prepaid.edit
transactions.*transactions.view, transactions.view_all
reports.*reports.view
tickets.*tickets.view, tickets.create, tickets.edit, tickets.delete
backups.*backups.view, backups.create, backups.edit, backups.restore, backups.delete
settings.*settings.view, settings.edit
audit.*audit.view
bandwidth.*bandwidth.view, bandwidth.create, bandwidth.edit, bandwidth.delete
cdn.*cdn.view, cdn.create, cdn.edit, cdn.delete
communication.*communication.view, communication.edit
fup.*fup.view, fup.reset
permissions.*permissions.view, permissions.create, permissions.edit, permissions.delete
sharing.*sharing.view, sharing.edit

Total is 220 default entries; your install may have added or removed some.

A few permissions end in _all and have special meaning: the user sees system-wide data instead of their normal reseller-scoped slice.

PermissionEffect
subscribers.view_allReseller sees every subscriber, not just own + sub-resellers’. Stat counters reflect global totals.
transactions.view_allReseller sees every transaction across the system.
sessions.view_allReseller sees every active session.
dashboard.view_adminShows admin-only dashboard widgets (System Metrics, Top Resellers).

These are typically granted to partner-style resellers who report up to the ISP owner but aren’t admins.

A permission group is a named bundle of permissions. Resellers and other reseller-tied users are assigned to one group; the group’s permissions are theirs.

Group fieldNotes
nameE.g. SALES, SUPPORT-L1, ACCOUNTING.
descriptionOptional.
permissionsM2M to permissions table via the permission_group_permissions junction.

The Permissions page lists groups with a column showing which resellers are assigned to each. The assignment lives on resellers.permission_group.

Admins bypass every permission check. This is implemented in middleware/auth.go:

if user.UserType == models.UserTypeAdmin {
return c.Next()
}

before any checkUserPermission() lookup. There is no way to restrict an admin’s actions via the permission system — to restrict an admin, downgrade them to support or another non-admin user_type.

A reseller whose permission_group is NULL or 0 gets all permissions — the same as admin in practice. This is the back-compat default so installs that haven’t set up groups still work. The moment you assign any group, the reseller’s permissions are limited to that group’s set.

When you change a reseller’s permission group, they do not need to log out. The frontend refreshes permissions on every page load via GET /api/auth/me. Steps:

  1. Admin saves the new group assignment.
  2. Reseller presses F5 (or just navigates).
  3. Frontend calls /api/auth/me.
  4. Backend returns the updated permissions array.
  5. authStore.refreshUser() saves to localStorage (so the new perms survive subsequent reloads — fixed in v1.0.165).
  6. UI re-renders with hidden/shown buttons reflecting the new set.

Two middleware functions in middleware/auth.go:

  • RequirePermission("subscribers.view") — caller must have this exact permission. Admins bypass.
  • RequireAnyPermission("subscribers.view", "subscribers.view_all") — caller must have at least one.

Routes are registered like:

sub := api.Group("/subscribers", middleware.RequirePermission("subscribers.view"))
sub.Delete("/:id", middleware.RequirePermission("subscribers.delete"))

A handler-level inline check is checkUserPermission(user, "...") used for permission gating inside bulk actions and similar mixed flows.

Two layers:

  • Route protectionPermissionRoute component in App.jsx shows “Access Denied” if the user lacks the required permission, before the page renders.
  • Button gating{hasPermission('...') && <Button />} hides UI when permission is missing. hasPermission is a selector on the Zustand auth store.

Both layers exist because the backend is the authoritative check; the frontend just hides UI to avoid confusing the user with buttons they can’t use.

These pages bypass the permission system entirely and only check user_type == admin. Resellers see “Access Denied” regardless of permissions:

  • /settings — system settings (but settings.view permission has been added in many routes)
  • /users — user management
  • /audit — audit log
  • /backups, /permissions, /change-bulk, /communication — historically admin-only, gradually being moved to permission-gated (see v1.0.165 changelog)

Check the source for the latest gating.

Lists every account in the users table. Columns: username, full name, email, user_type, reseller (if any), is_active, last_login, actions.

Add User opens a form: username, password (with eye-icon show/hide toggle), email, user_type dropdown, reseller association (if non-admin user_type), is_active toggle.

Create a support agent who can read everything but change nothing

Section titled “Create a support agent who can read everything but change nothing”
  1. Permissions → Add Group → name SUPPORT-READ-ONLY.
  2. Tick *.view and *.view_all permissions (subscribers, sessions, transactions, reports).
  3. Save.
  4. Users → Add User → user_type support, assign permission group SUPPORT-READ-ONLY.
  1. Open their permission group (or create a new one called PARTNER).
  2. Tick subscribers.view_all, transactions.view_all, sessions.view_all.
  3. Save.
  4. Tell the reseller to press F5. They now see system-wide totals on the dashboard but still can’t modify other resellers’ subscribers.

Lock down a junior reseller to “renew only”

Section titled “Lock down a junior reseller to “renew only””
  1. New permission group JR-SALES.
  2. Tick: subscribers.view, subscribers.renew, subscribers.create, transactions.view, dashboard.view_admin.
  3. Assign the reseller to this group.
  4. They can create + renew but can’t disconnect, delete, or change services.
PermissionEffect
permissions.viewPermissions page loads.
permissions.createAdd Group button.
permissions.editEdit group + assign permissions.
permissions.deleteDelete group (refused if resellers still assigned).

These are admin-controlled in practice.

  • Resellers — assigns permission groups to reseller accounts.
  • Audit — logs every permission check failure (SECURITY: ... lines).
  • Settings — admin-only system settings page.
  • Subscribers — where the bulk-action permission checks live.
  • Dashboard — examples of *.view_all widening behaviour.