Security
Reference
Public
Research
Research evidence behind the ERPat Security user guide — the live module source files (manifest, config, controllers, models, migrations, views, language) read to verify every fact about all four tools, including the Device Management move in from core.
Guide version: r2
Module version: 1.4.0 Updated: 2026-08-28 Estimated time: 14 min 8 views 0% helpful
Administration & Reference
Research
This guide was written from the module's own source, not from assumptions. This
page lists the files that were read and the verified facts each one established — so a future
editor can re-check the guide against reality.
????
Documentation-only. Nothing on this page (or anywhere in modules/Security/docs/)
is served by a route or consumed by PHP — it exists to keep the guide honest and traceable.
Manifest & module metadata
| File | Established |
module.json | Version 3.0.0; type feature; the module owns four tools end-to-end; the six tables and their migrations; the permission list; user_guide: true (docs sync into the User Guide portal); the "core protection layer stays in core" risk note; the risk note naming the two models core loads on the hot path (OffsiteAccessModel and Banned_devices_model) and the note that module_device_management was retired into module_security. |
config/module_config.php | module_key = security (matches slug); can_disable = false — the umbrella is locked on. |
Config sidecars
| File | Established |
config/menu.php | Four items in menu order — Device Management (perm device_management, fa-mobile), Offsite Access (offsite_access, fa-globe), Locked Accounts (locked_accounts, fa-shield), Access Logs (access_logs, fa-history). No per-item setting keys: the whole slice is gated on module_security and each leaf is then purely permission-gated — the legacy module_offsite_access / module_access (2.0.0) and module_device_management (3.0.0) sub-toggles are all retired. |
config/default_menu.php | The four items under a Security header (position 160), in that order. The module now owns the whole section — with Device Management moved in, the core slice in application/config/left_menu.php was emptied and removed, so there is no core group left to coalesce with. |
config/permissions.php | Every RBAC group and its exact category/action; Offsite Access is a dropdown (default_level: specific), Active Sessions / Access Logs / Device Management / Device Management Groups are module-level, Locked Accounts is a simple View, and Block/Unblock Device are simple Enabled permits. Keys unchanged from core (no data migration) — device_management is the one genuinely new key. |
config/routes.php | The flat URL → controller map, incl. the lowercase routes for the mixed-case/underscore controllers, and the unchanged device_management / device_management_groups URLs. |
config/system_logs.php | The 19 audit-log event keys and their severities — the original 14 plus the five Device Management events added in 3.0.0; note that offsite_access:ip_security stays core-owned (emitted by IP-restriction middleware). |
Controllers
| File | Established |
controllers/Device_management.php | Constructor gates module_security + device_management — the page-level gate that did not exist before; index() renders the two-tab page; list_data() returns an empty list until a person/team filter is posted, then reads Banned_devices_model::get_devices_with_ban_status(); the ban action requires block_device, the restore action unblock_device; both write audit rows via _log_device_action(). |
controllers/Device_management_groups.php | The Groups tab — an ajax-tab partial, so index() uses load->view(), not template->rander(); gated on module_security + device_management_groups with _create/_update/_delete on the actions; create/update/delete audited. |
controllers/OffsiteAccess.php | Constructor gates module_security + offsite_access (the retired module_offsite_access sub-toggle is gone — every controller in the module now passes the same security slug); save() creates one grant per selected user (multi-select) and writes created:offsite_access; edit/delete write updated/deleted; the list captures old values before update/delete. |
controllers/Locked_Accounts.php | Constructor allows locked_accounts OR core staff_support; clear_attempts() resets disable_login/login_attempts/last_login_attempts, notifies the user, logs unlocked:locked_account; the Active Sessions tab reads live sessions and logs logout:active_session / deleted:active_session. |
controllers/Access_logs.php | The tabbed page shell (index → access/index), the Logs tab (view/list_data), a log_action creator, and delete logging deleted:access_log. |
controllers/Access_devices.php | Auto-generates api_key (UUID) + hashed api_secret on create; generate_new_secret() rotates and logs the critical rotated_secret:access_device (secret never logged); passes = staff allowed through; create/update/delete audited. |
controllers/Access_device_categories.php | Category CRUD gated on access_logs; create/update/delete audited. |
Models & migrations
| File | Established |
models/OffsiteAccessModel.php | The grants model — also read by the core IP-restriction middleware to resolve active off-network exceptions. |
models/Banned_devices_model.php | The banned_devices table (one soft-deletable row per user + user agent). get_details() is called by core application/models/Users_model::authenticate() on every sign-in attempt — so the model must not be renamed. get_devices_with_ban_status() was absorbed from core System_logs_model::get_system_logs_data_per_device(); it takes the latest new_login log per (user, user agent) via a self-join anti-join and LEFT JOINs the ban row to derive the status. |
models/Device_management_groups_model.php | The device_management_groups table (uuid, title, user_agent, status) behind the Groups tab and the group filter. |
models/Access_devices_model.php, Access_device_categories_model.php, Access_logs_model.php | The Access Device Logs data layer (list/detail/save/delete). |
migrations/…_create_offsite_access_table.php | Columns for offsite_access (uuid, user_id, start/end date, audit block); guarded CREATE-IF-NOT-EXISTS. |
migrations/…_create_access_device_categories_table.php | Columns for access_device_categories (title, detail, status). |
migrations/…_create_access_devices_table.php | Columns for access_devices (api_key, api_secret, device_name, passes, remarks, category_id, labels, status). |
migrations/…_create_access_logs_table.php | Columns for access_logs (device_id, user_id, remarks, timestamp). |
migrations/20260730213155_create_banned_devices_table.php | Columns for banned_devices (user_id, http_agent, ip_address, remarks, who banned it); guarded CREATE-IF-NOT-EXISTS — the table already ships in the base-install schema, so this is a no-op on existing databases. |
migrations/20260730213156_create_device_management_groups_table.php | Columns for device_management_groups (uuid, title, user_agent, status); same idempotent guard. |
application/migrations/20260730213157_retire_device_management_setting.php | Core-side retirement: carries a tenant's module_device_management = 1 forward onto module_security, then drops the orphaned row so the toggle no longer appears in Manage Modules. |
Views & language
| File | Established |
views/device_management/index.php | The two-tab Device Management shell — Entries · Groups — and the Entries filter bar: the person/team picker the list needs before it shows anything, plus the Status (defaulting to Active) and Device Group filters. The Groups tab only renders for holders of device_management_groups. |
views/device_management/ban_device_modal_form.php, restore_device_modal_form.php | The ban dialog (Remarks is a required field) and the restore confirmation. |
views/device_management/groups/index.php, groups/modal_form.php | The Groups list and its Title / User Agent / Status form. |
views/access/index.php | The three-tab Access Device Logs shell — Logs · Devices · Categories (ajax-tab, lazy-loaded). |
views/locked_accounts/index.php | The two-tab shell — Browse (Locked Accounts) · Active Sessions. |
views/offsite_access/*, views/access/devices/*, views/active_sessions/* | The grant form, the device forms incl. the credential dialog, the passes list, and the session-viewer modal. |
language/english/security_lang.php | The feature strings — including the Device Management vocabulary moved in from core (ban_device, unban_device, unban) — and the four nav_desc_* hover descriptions; documents which vocabulary stays core-shared. |
Device Management — evidence for the move in from core (3.0.0)
Device Management was a built-in ERPat screen (application/controllers/Device_management.php
and friends) until 3.0.0, when it became this module's fourth tool. Because a migration is
exactly where a guide is most likely to repeat a stale claim, the files below were read
end-to-end and the findings recorded verbatim.
Files inspected
- Controllers —
controllers/Device_management.php, controllers/Device_management_groups.php
- Models —
models/Banned_devices_model.php, models/Device_management_groups_model.php, and the core caller application/models/Users_model.php
- Views —
views/device_management/index.php, ban_device_modal_form.php, restore_device_modal_form.php, views/device_management/groups/index.php, groups/modal_form.php
- Config —
config/routes.php, config/menu.php, config/permissions.php, config/system_logs.php
- Migrations —
migrations/20260730213155_create_banned_devices_table.php, migrations/20260730213156_create_device_management_groups_table.php
What those files established
| Finding | Evidence |
The URLs are unchanged. Existing links, bookmarks and in-view get_uri() calls keep resolving. | config/routes.php declares device_management → Device_management/index and device_management_groups → Device_management_groups/index (plus their (:any) forms) — the same URIs the core version served, now owned by the module. |
The permission keys are unchanged, so serialized grants in users.permissions / roles.permissions keep resolving with no data migration. | config/permissions.php re-declares device_management_groups (+ _create/_update/_delete), block_device and unblock_device with the original category/action labels; the matching core rows were deleted from application/helpers/permission_catalog_helper.php in the same change (a core row shadows a module declaration, so leaving them would have kept this block inert). |
| The page previously had no permission gate. Any signed-in staff member could open it by typing the address; only the menu link was gated, on a key nothing declared — so it was ungrantable and effectively admin-only. | The comment block in config/permissions.php records that Left_menu already called current_has_permit('device_management') while no catalog row ever declared the key. device_management is newly declared, and controllers/Device_management.php now calls with_permission("device_management", "redirect") in its constructor — the gate that makes it real and grantable. |
| The ban check runs pre-authentication, in core. It rejects a person + browser pair at sign-in; it does not disable the account. | application/models/Users_model::authenticate() loads Banned_devices_model and matches user_id against the raw HTTP_USER_AGENT header before the sign-in succeeds. The model resolves through module package paths and loads regardless of the module's enabled state, so the check never goes dark — which is also why the model must not be renamed, and why every option in it is escaped or cast. |
| The list is derived, not stored. No "is banned" flag exists anywhere. | Banned_devices_model::get_devices_with_ban_status() (absorbed from core System_logs_model::get_system_logs_data_per_device()) takes the latest new_login row per user + user agent out of system_logs via a self-join anti-join, then LEFT JOINs banned_devices and computes status as banned / active from whether that join matched. |
| Banning and restoring are now audited (they were not before). | config/system_logs.php declares banned:banned_device (critical) and unbanned:banned_device (warning), plus created/updated/deleted:device_management_group; Device_management::_log_device_action() emits the first two. |
The module_device_management toggle is retired, so the tool no longer has its own Manage Modules row — visibility is purely a permission decision. | config/menu.php carries no setting key on the item; the manifest's risk notes list the retirement, and application/migrations/20260730213157_retire_device_management_setting.php carries the old value forward onto module_security before dropping the row. |
| The tables ship in the base-install schema, so the module migrations are guards rather than first-time creators. | Both …_create_banned_devices_table.php and …_create_device_management_groups_table.php are idempotent CREATE-IF-NOT-EXISTS — no-ops on existing databases, correct for fresh installs and newly provisioned tenants. They bring the module's table count to six. |
⚠️
Two SQL-injection vulnerabilities were fixed in the migrated code. The serious one was
reachable before authentication: the core version interpolated the browser's
User-Agent header straight into the sign-in query. Banned_devices_model
now escapes or casts every option, and rebuilds the user-id list from integers. Keep it that way.
Scope note — what was verified NOT to be here
The guide's central claim — that this module is tooling, not the protection layer — was
confirmed by the manifest's own risk notes and the config comments: guards
(application/guards/), the middleware pipeline
(application/middleware/), the Roles/permissions RBAC editor, and the audit-log
store all remain in core. The traffic between the two runs the other way: core reads this
module's data — the IP-restriction middleware reads the offsite grants, and the sign-in check in
Users_model::authenticate() reads the device ban list. This module supplies those
lists, contributes permissions, and writes audit rows — it does not own or gate the core controls
that consume them.
Next steps