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
You are viewing version r2 of this guide. View the current version
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

FileEstablished
module.jsonVersion 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.phpmodule_key = security (matches slug); can_disable = false — the umbrella is locked on.

Config sidecars

FileEstablished
config/menu.phpFour 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.phpThe 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.phpEvery 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.phpThe 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.phpThe 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

FileEstablished
controllers/Device_management.phpConstructor gates module_security + device_managementthe 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.phpThe 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.phpConstructor 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.phpConstructor 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.phpThe tabbed page shell (indexaccess/index), the Logs tab (view/list_data), a log_action creator, and delete logging deleted:access_log.
controllers/Access_devices.phpAuto-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.phpCategory CRUD gated on access_logs; create/update/delete audited.

Models & migrations

FileEstablished
models/OffsiteAccessModel.phpThe grants model — also read by the core IP-restriction middleware to resolve active off-network exceptions.
models/Banned_devices_model.phpThe 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.phpThe 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.phpThe Access Device Logs data layer (list/detail/save/delete).
migrations/…_create_offsite_access_table.phpColumns for offsite_access (uuid, user_id, start/end date, audit block); guarded CREATE-IF-NOT-EXISTS.
migrations/…_create_access_device_categories_table.phpColumns for access_device_categories (title, detail, status).
migrations/…_create_access_devices_table.phpColumns for access_devices (api_key, api_secret, device_name, passes, remarks, category_id, labels, status).
migrations/…_create_access_logs_table.phpColumns for access_logs (device_id, user_id, remarks, timestamp).
migrations/20260730213155_create_banned_devices_table.phpColumns 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.phpColumns for device_management_groups (uuid, title, user_agent, status); same idempotent guard.
application/migrations/20260730213157_retire_device_management_setting.phpCore-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

FileEstablished
views/device_management/index.phpThe 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.phpThe ban dialog (Remarks is a required field) and the restore confirmation.
views/device_management/groups/index.php, groups/modal_form.phpThe Groups list and its Title / User Agent / Status form.
views/access/index.phpThe three-tab Access Device Logs shell — Logs · Devices · Categories (ajax-tab, lazy-loaded).
views/locked_accounts/index.phpThe 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.phpThe 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

  • Controllerscontrollers/Device_management.php, controllers/Device_management_groups.php
  • Modelsmodels/Banned_devices_model.php, models/Device_management_groups_model.php, and the core caller application/models/Users_model.php
  • Viewsviews/device_management/index.php, ban_device_modal_form.php, restore_device_modal_form.php, views/device_management/groups/index.php, groups/modal_form.php
  • Configconfig/routes.php, config/menu.php, config/permissions.php, config/system_logs.php
  • Migrationsmigrations/20260730213155_create_banned_devices_table.php, migrations/20260730213156_create_device_management_groups_table.php

What those files established

FindingEvidence
The URLs are unchanged. Existing links, bookmarks and in-view get_uri() calls keep resolving.config/routes.php declares device_managementDevice_management/index and device_management_groupsDevice_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

Report a content problem