Security Reference Public

Reference

Technical reference for the ERPat Security module — controllers, routes, models, views, migrations, database tables and columns, permission keys, menu entries, and system-audit-log event keys for Device Management, Offsite Access, Locked Accounts, Active Sessions, and Access Device Logs.

Guide version: r3 Module version: 1.5.0 Updated: 2026-08-28 Estimated time: 14 min 10 views 0% helpful
Administration & Reference

Reference

The data-and-screen reference for the Security module's four admin tools — controllers, routes, models, views, migrations, tables, permission keys, menu entries, and audit-log events. Everything here is drawn from the live module source; see Research for the exact files.

Controllers

ControllerToolKey methods
Device_management Device Management — Entries tab (page shell) index (tabbed shell), list_data, ban_device_modal_form, ban_device, restore_device_modal_form, restore_device
Device_management_groups Device Management — Groups tab index (ajax-tab partial), list_data, modal_form, save, delete
OffsiteAccess Offsite Access index, modalForm, save, listData, delete
Locked_Accounts Locked Accounts + Active Sessions index, locked_accounts_view, active_sessions_view, list_data, clear_attempts, list_sessions_data, view_sessions_modal, logout_user_all_devices, delete_session, get_session_statistics
Access_logs Access Device Logs — Logs tab (page shell) index (tabbed shell), view, list_data, log_action, delete
Access_devices Access Device Logs — Devices tab index, list_data, modal_form, modal_form_credential, generate_new_secret, save, delete, pass_list
Access_device_categories Access Device Logs — Categories tab index, list_data, modal_form, save, delete

All seven extend App_Controller (staff, session-authenticated). Every constructor opens with the module's single always-on guard with_module('security') — there are no per-tool module_* sub-toggles left — and then applies its own permission: Device Management on device_management; its Groups tab on device_management_groups; Offsite Access on offsite_access; all three Access Device controllers on access_logs; Locked Accounts on locked_accounts (or the core staff_support).

Per-action gates sit on the individual methods: block_device on ban_device_modal_form + ban_device, and unblock_device on restore_device_modal_form + restore_device. The row builder (make_row) checks the same two keys, so a user without them sees the row but no action button.

⚠️
Device_management::__construct() is a new gate. The core version of this controller had no permission check on direct URL access — any signed-in staff member could open /device_management by typing the address, and only the sidebar link was gated, on a device_management key that no catalog row ever declared (so it was ungrantable and the item was effectively admin-only). The key is now declared by this module and enforced in the constructor, so it can be granted to a role and it actually blocks direct access.

Routes

Declared in modules/Security/config/routes.php (mixed-case/underscore controllers need explicit lowercase routes). The (:any) catch-alls map to controller methods.

URLResolves to
device_managementDevice_management/index
device_management/(:any)Device_management/$1
device_management_groupsDevice_management_groups/index
device_management_groups/(:any)Device_management_groups/$1
offsite_accessOffsiteAccess/index
offsite_access/(:any)OffsiteAccess/$1
locked_accountsLocked_Accounts/index
locked_accounts/(:any) · /(:any)/(:any)Locked_Accounts/$1 · $1/$2
access_logs · access_logs/(:any)Access_logs/index · $1
access_devices · access_devices/(:any)Access_devices/index · $1
access_device_categories · …/(:any)Access_device_categories/index · $1
ℹ️
The Device Management URLs are unchanged. /device_management and /device_management_groups are byte-for-byte the addresses the core version used — only the file that declares them moved (from application/config/routes.php into modules/Security/config/routes.php). Existing links, bookmarks and in-view get_uri() calls keep resolving. device_management_groups cannot be swallowed by device_management/(:any) — CodeIgniter anchors each route key and (:any) never matches across the missing /.

Models

Six models, all extending Crud_model and living in modules/Security/models/. All SQL lives here — never in a controller or view.

ModelTool / tableNotes
Banned_devices_model Device Management — banned_devices get_details, get_devices_with_ban_status, and pre_auth_checkthe sign-in check that runs on every attempt, which core reaches through this module's own config/auth_checks.php declaration (see the callout below).
Device_management_groups_model Device Management — device_management_groups get_details. Backs the Groups tab list and the group filter dropdown on the Entries tab.
OffsiteAccessModel Offsite Access — offsite_access Also read by the core IP-restriction middleware when deciding whether to allow an off-network request.
Access_logs_model Access Device Logs — access_logs The access-event history.
Access_devices_model Access Device Logs — access_devices Registered physical devices and their API credentials.
Access_device_categories_model Access Device Logs — access_device_categories Device categories.
⚠️
The sign-in check is a DECLARATION — core no longer names this module. Core Users_model::authenticate() used to load Banned_devices_model by name. It no longer names this module, or any module: it calls erpat_run_auth_checks(), which resolves whatever each module declared in its own config/auth_checks.php. This module declares Banned_devices_model::pre_auth_check(), and that is what actually rejects a banned device — still on every sign-in attempt, still on the (user + user agent) pair. For a maintainer the practical rule is unchanged but now has one extra file in it: renaming the model, the method, or the banned_devices table breaks sign-in for the whole tenant unless config/auth_checks.php is updated in the same change. Because core can no longer detect that mismatch, the module's own test suite pins the sidecar and its declared target. In the other direction the coupling was removed: get_devices_with_ban_status() absorbed core's System_logs_model::get_system_logs_data_per_device(), so core no longer queries this module's tables to build the list. OffsiteAccessModel is the remaining exception — it is still loaded directly by class name from core IpRestrictionMiddleware, not through this hook.

Sign-in check registration

Declared in modules/Security/config/auth_checks.php. Core Users_model::authenticate() calls erpat_run_auth_checks() (in application/helpers/module_compatibility_helper.php), which reads every module's config/auth_checks.php, resolves each declaration and runs it in priority order. This module declares exactly one entry.

KeyValueMeaning
idsecurity.banned_deviceGlobally unique — the first registration of an id wins
model · methodBanned_devices_model · pre_auth_checkWhat core loads and calls; resolves through the module package paths, so it works regardless of the module's enabled state
stagepre_credentialRuns before the password is compared — the exact point the old hardcoded core branch sat, so a banned device is still refused without burning a login attempt
priority10Ascending order within the stage
requiredtrueIf the check cannot run, the sign-in is denied — never silently skipped

pre_auth_check() returns nothing to allow the sign-in, or a verdict to deny it. Its denial code device_banned is one core routes verbatim, so authenticate() returns the same literal sentinel it always did and nothing a user sees has changed: the sign-in form still shows "Access from this device is restricted." and still writes a banned_device_attempt activity-log row. A code core does not route is downgraded to a plain authentication failure. If the banned_devices table is missing — a database that never ran php erpat migrate:modules — the check logs that and denies, rather than failing open or fataling.

⚠️
The registry is FAIL-CLOSED, and this check is deliberately NOT gated on module_security. Fail-closed: a declared check that cannot be resolved, that throws, or that returns something outside the contract denies the sign-in, and if the scan for the sidecars is itself degraded then every sign-in is denied — a discovery problem can never quietly reduce to "no checks". Ungated: this is the one surface in the module that does not honour the module switch, because gating an enforcement deny-check would turn the Manage Modules toggle into an authentication bypass, and the setting lookup fails open when the row is missing. Core also cannot read the right tenant's setting on that path — sign-in swaps to the tenant database without reloading settings. A check that needs to be conditional decides that inside its own method, where the connection is unambiguous.

Views

19 view files under modules/Security/views/, namespaced one directory deep so the loader cascade cannot collide with another module.

ViewScreen
device_management/index.phpDevice Management page shell + the Entries tab (filters, list, action buttons)
device_management/ban_device_modal_form.phpBan-device modal — the required Remarks/reason field
device_management/restore_device_modal_form.phpRestore (unban) confirmation modal
device_management/groups/index.phpGroups tab list (loaded as an ajax-tab partial)
device_management/groups/modal_form.phpGroup add / edit modal — Title, User Agent, Status
offsite_access/index.php · modal_form.phpOffsite Access list and grant form
locked_accounts/index.php · locked_accounts_tab.php · active_sessions_tab.phpLocked Accounts page shell and its two tabs
active_sessions/view_sessions_modal.phpPer-user session detail modal
access/index.php · logs/index.php · devices/* · categories/*Access Device Logs shell and its Logs · Devices · Categories tabs (8 files)

Database tables

Six tables, created only by this module's own idempotent migrations (tracked in migrations_security) — none of them ships in the base-install schema. All carry the standard updated_at + deleted audit columns.

banned_devices

One row per active ban — a person plus the browser (user agent) they may no longer sign in from. Restoring a device soft-deletes its row.

ColumnTypeNotes
idint unsignedPK
user_idintThe person who is blocked from this device
http_agenttextThe browser / user-agent string being banned
ip_addressvarchar(45)IP captured at the time of the ban
remarkstextThe reason — required on the ban form
created_byintAdministrator who applied the ban
date_createddatetimeUTC ban time
updated_at · deletedtimestamp · tinyintAudit + soft delete (a restore sets deleted)

Indexed (user_id, http_agent(191), deleted) — that exact triple is the lookup Banned_devices_model::pre_auth_check() runs on every sign-in attempt (core reaches it through the sign-in check declaration), so it carries a covering index.

device_management_groups

Named user-agent groups used to label and filter the Entries list. A group grants nothing and blocks nothing.

ColumnTypeNotes
idint unsignedPK
uuidvarchar(64)Per-group UUID
titlevarchar(150)Group name (e.g. "Company Chromebooks")
user_agenttextThe user-agent string the group matches
statusvarchar(20)active / inactive
created_byintWho created the group
date_createddatetimeUTC create time
updated_at · deletedtimestamp · tinyintAudit + soft delete

Indexed on (status, deleted) and on user_agent(191), which is joined against system_logs.http_agent to bucket sign-in devices.

ℹ️
The Device Management list is DERIVED — there is no stored "is banned" flag. Each row on the Entries tab is built at read time by taking the most recent sign-in log per person per user agent (from the core system_logs sign-in rows) and joining it to banned_devices. The Active / Banned status is simply "does a live ban row exist for this person + user agent". Nothing is written to any table when a device merely appears in the list — only banning and restoring write, and they write to banned_devices alone.

offsite_access

ColumnTypeNotes
idint unsignedPK
uuidvarchar(36)Per-grant UUID (v4)
user_idintStaff member granted the window
start_date / end_datedateThe exception window
date_createddatetimeUTC create time
created_byintGranting admin
updated_at · deletedtimestamp · tinyintAudit + soft delete

access_device_categories

ColumnTypeNotes
idint unsignedPK
titlevarchar(150)Category name
detailtextOptional description
statustinyint1 = active
updated_at · deletedtimestamp · tinyintAudit + soft delete

access_devices

ColumnTypeNotes
idint unsignedPK
api_keyvarchar(36)UUID the device presents
api_secretvarchar(120)Hashed secret (never shown/logged in clear)
device_namevarchar(36)Display name
passestextComma-separated staff user IDs allowed through
remarkstextOptional notes
category_idintFK-by-value to access_device_categories
labelstextLabels
statustinyint1 = active
updated_at · deletedtimestamp · tinyintAudit + soft delete

access_logs

ColumnTypeNotes
idint unsignedPK
device_idintWhich device recorded the event
user_idintWho accessed
remarkstextOptional (e.g. entry / exit)
timestampdatetimeUTC event time (shown local)
updated_at · deletedtimestamp · tinyintAudit + soft delete
ℹ️
Locked Accounts / Active Sessions has no dedicated table. The Locked Accounts tab reads the failed-attempt columns on users (disable_login, login_attempts, last_login_attempts); the Active Sessions tab reads the core session store (ci_sessions) through the core session helper.

Migrations

Six module migrations in modules/Security/migrations/, run by php erpat migrate:modules against the per-module migrations_security version table. They are the only source of these six tables — nothing here ships in the base-install schema — and every one is written as an idempotent guard (table_exists() → return, then CREATE TABLE IF NOT EXISTS), so re-running them against a database that already has the tables is a no-op. All are reversible. A database that has never run migrate:modules therefore has no banned_devices table, and the sign-in check treats that as a reason to deny — see Sign-in check registration.

MigrationCreates
20260727100254_create_offsite_access_table.phpoffsite_access
20260727100255_create_access_device_categories_table.phpaccess_device_categories
20260727100256_create_access_devices_table.phpaccess_devices
20260727100257_create_access_logs_table.phpaccess_logs
20260730213155_create_banned_devices_table.phpbanned_devices (+ the covering sign-in index)
20260730213156_create_device_management_groups_table.phpdevice_management_groups

Retirement migrations live in CORE (they touch the shared settings table, so they belong in application/migrations/, not here): 20260730213157_retire_device_management_setting.php carries any module_device_management = 1 forward onto module_security and then drops the orphaned row; 20260730204800_retire_core_submodule_settings.php did the same for module_offsite_access and module_access in the previous release. Nothing is lost — the tools simply stop having their own on/off rows.

Permission keys

KeyRoles-editor group / actionStyle
device_managementDevice Management: Module / AccessModule-level — new in 3.0.0
device_management_groupsDevice Management: Groups / EnabledModule-level
device_management_groups_create · _update · _deleteDevice Management: Groups / Create · Edit · RemoveChild
block_deviceDevice Management: Block Device / EnabledSimple (no children)
unblock_deviceDevice Management: Unblock Device / EnabledSimple (no children)
offsite_accessSecurity: Offsite Access / AccessDropdown (all / specific)
offsite_access_create · _update · _deleteSecurity: Offsite Access / Create · Edit · RemoveChild
locked_accountsSecurity: Locked Accounts / ViewSimple (no children)
active_sessionsSecurity: Active Sessions / ManageModule-level
active_sessions_create · _update · _deleteSecurity: Active Sessions / Create · Edit · RemoveChild
access_logsSecurity: Access Logs / EnabledModule-level
access_logs_create · _update · _deleteSecurity: Access Logs / Create · Edit · RemoveChild

Keys are unchanged from the core declaration. device_management_groups, block_device and unblock_device keep the exact key names, categories and actions they had as core rows, so serialized grants already stored in users.permissions / roles.permissions keep resolving with no data migration. The legacy 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 the module's block inert. Admins bypass every permission check.

Not owned here: staff_support (Staffing) and api_clients (Security: API Clients) remain core-owned; the core IP-restriction block event offsite_access:ip_security is emitted by core middleware, not this module.

Settings & menu

SettingRole
module_securityThe module's one and only enable setting — seeded on, can_disable=false, so it never appears as a switchable row in Manage Modules
module_device_managementRetired in 3.0.0 — carried forward onto module_security, then dropped
module_offsite_accessRetired in 2.0.0 — dropped
module_accessRetired in 2.0.0 — dropped
⚠️
There are no per-tool on/off switches. Every retired module_* row above is gone from Settings → Manage Modules; the module has exactly one always-on setting, module_security. Who sees Device Management, Offsite Access, Locked Accounts or Access Logs is purely a permissions decision in Settings → Roles. Device Management used to ship off for a brand-new tenant, so nobody could open it; it now ships reachable by admins and by roles explicitly granted device_management. No non-admin gains access on upgrade.

Menu entries (config/menu.php), in rendered order: Device Management (fa-mobile, position 160) · Offsite Access (fa-globe, 161) · Locked Accounts (fa-shield, 162) · Access Logs (fa-history, 163). Since Device Management moved in, this module owns the whole Security sidebar section — the core slice in application/config/left_menu.php was emptied and removed, so there is no longer a core group to coalesce with (default_menu.php, position 160).

Audit-log events

Keyed action:component in config/system_logs.php, merged into the core audit-log config. Written on every sensitive mutation.

Event keySeverityMeaning
banned:banned_devicecriticalA user's sign-in device was banned
unbanned:banned_devicewarningA user's sign-in device ban was lifted
created:device_management_groupinfoDevice group created
updated:device_management_groupinfoDevice group updated
deleted:device_management_groupwarningDevice group deleted
created:offsite_accesswarningOffsite window granted
updated:offsite_accesswarningOffsite window updated
deleted:offsite_accesswarningOffsite window revoked
unlocked:locked_accountwarningAccount unlocked (attempts cleared)
logout:active_sessionwarningAll sessions terminated for a user
deleted:active_sessionwarningSingle session terminated
created:access_deviceinfoDevice registered
updated:access_deviceinfoDevice updated
deleted:access_devicewarningDevice deleted
rotated_secret:access_devicecriticalDevice API secret regenerated
created:access_device_categoryinfoCategory created
updated:access_device_categoryinfoCategory updated
deleted:access_device_categorywarningCategory deleted
deleted:access_logwarningAccess log entry deleted
The five Device Management events are new in 3.0.0. Banning and restoring a device were completely unaudited in the core version of this feature — banning locks a person out of ERPat from that browser, so it now leaves a row in the tenant's own audit log (banned:banned_device is rated critical). They appear in Settings → System Logs under category Security, module Device Management.

Next steps

Was this guide helpful?

Report a content problem