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
| Controller | Tool | Key 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.
| URL | Resolves to |
|---|---|
device_management | Device_management/index |
device_management/(:any) | Device_management/$1 |
device_management_groups | Device_management_groups/index |
device_management_groups/(:any) | Device_management_groups/$1 |
offsite_access | OffsiteAccess/index |
offsite_access/(:any) | OffsiteAccess/$1 |
locked_accounts | Locked_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 |
/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.
| Model | Tool / table | Notes |
|---|---|---|
Banned_devices_model |
Device Management — banned_devices |
get_details, get_devices_with_ban_status, and
pre_auth_check — the 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. |
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.
| Key | Value | Meaning |
|---|---|---|
id | security.banned_device | Globally unique — the first registration of an id wins |
model · method | Banned_devices_model · pre_auth_check | What core loads and calls; resolves through the module package paths, so it works regardless of the module's enabled state |
stage | pre_credential | Runs 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 |
priority | 10 | Ascending order within the stage |
required | true | If 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.
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.
| View | Screen |
|---|---|
device_management/index.php | Device Management page shell + the Entries tab (filters, list, action buttons) |
device_management/ban_device_modal_form.php | Ban-device modal — the required Remarks/reason field |
device_management/restore_device_modal_form.php | Restore (unban) confirmation modal |
device_management/groups/index.php | Groups tab list (loaded as an ajax-tab partial) |
device_management/groups/modal_form.php | Group add / edit modal — Title, User Agent, Status |
offsite_access/index.php · modal_form.php | Offsite Access list and grant form |
locked_accounts/index.php · locked_accounts_tab.php · active_sessions_tab.php | Locked Accounts page shell and its two tabs |
active_sessions/view_sessions_modal.php | Per-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.
| Column | Type | Notes |
|---|---|---|
id | int unsigned | PK |
user_id | int | The person who is blocked from this device |
http_agent | text | The browser / user-agent string being banned |
ip_address | varchar(45) | IP captured at the time of the ban |
remarks | text | The reason — required on the ban form |
created_by | int | Administrator who applied the ban |
date_created | datetime | UTC ban time |
updated_at · deleted | timestamp · tinyint | Audit + 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.
| Column | Type | Notes |
|---|---|---|
id | int unsigned | PK |
uuid | varchar(64) | Per-group UUID |
title | varchar(150) | Group name (e.g. "Company Chromebooks") |
user_agent | text | The user-agent string the group matches |
status | varchar(20) | active / inactive |
created_by | int | Who created the group |
date_created | datetime | UTC create time |
updated_at · deleted | timestamp · tinyint | Audit + soft delete |
Indexed on (status, deleted) and on user_agent(191), which is joined
against system_logs.http_agent to bucket sign-in devices.
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
| Column | Type | Notes |
|---|---|---|
id | int unsigned | PK |
uuid | varchar(36) | Per-grant UUID (v4) |
user_id | int | Staff member granted the window |
start_date / end_date | date | The exception window |
date_created | datetime | UTC create time |
created_by | int | Granting admin |
updated_at · deleted | timestamp · tinyint | Audit + soft delete |
access_device_categories
| Column | Type | Notes |
|---|---|---|
id | int unsigned | PK |
title | varchar(150) | Category name |
detail | text | Optional description |
status | tinyint | 1 = active |
updated_at · deleted | timestamp · tinyint | Audit + soft delete |
access_devices
| Column | Type | Notes |
|---|---|---|
id | int unsigned | PK |
api_key | varchar(36) | UUID the device presents |
api_secret | varchar(120) | Hashed secret (never shown/logged in clear) |
device_name | varchar(36) | Display name |
passes | text | Comma-separated staff user IDs allowed through |
remarks | text | Optional notes |
category_id | int | FK-by-value to access_device_categories |
labels | text | Labels |
status | tinyint | 1 = active |
updated_at · deleted | timestamp · tinyint | Audit + soft delete |
access_logs
| Column | Type | Notes |
|---|---|---|
id | int unsigned | PK |
device_id | int | Which device recorded the event |
user_id | int | Who accessed |
remarks | text | Optional (e.g. entry / exit) |
timestamp | datetime | UTC event time (shown local) |
updated_at · deleted | timestamp · tinyint | Audit + soft delete |
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.
| Migration | Creates |
|---|---|
20260727100254_create_offsite_access_table.php | offsite_access |
20260727100255_create_access_device_categories_table.php | access_device_categories |
20260727100256_create_access_devices_table.php | access_devices |
20260727100257_create_access_logs_table.php | access_logs |
20260730213155_create_banned_devices_table.php | banned_devices (+ the covering sign-in index) |
20260730213156_create_device_management_groups_table.php | device_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
| Key | Roles-editor group / action | Style |
|---|---|---|
device_management | Device Management: Module / Access | Module-level — new in 3.0.0 |
device_management_groups | Device Management: Groups / Enabled | Module-level |
device_management_groups_create · _update · _delete | Device Management: Groups / Create · Edit · Remove | Child |
block_device | Device Management: Block Device / Enabled | Simple (no children) |
unblock_device | Device Management: Unblock Device / Enabled | Simple (no children) |
offsite_access | Security: Offsite Access / Access | Dropdown (all / specific) |
offsite_access_create · _update · _delete | Security: Offsite Access / Create · Edit · Remove | Child |
locked_accounts | Security: Locked Accounts / View | Simple (no children) |
active_sessions | Security: Active Sessions / Manage | Module-level |
active_sessions_create · _update · _delete | Security: Active Sessions / Create · Edit · Remove | Child |
access_logs | Security: Access Logs / Enabled | Module-level |
access_logs_create · _update · _delete | Security: Access Logs / Create · Edit · Remove | Child |
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
| Setting | Role |
|---|---|
module_security | The 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_management | Retired in 3.0.0 — carried forward onto module_security, then dropped |
module_offsite_access | Retired in 2.0.0 — dropped |
module_access | Retired in 2.0.0 — dropped |
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 key | Severity | Meaning |
|---|---|---|
banned:banned_device | critical | A user's sign-in device was banned |
unbanned:banned_device | warning | A user's sign-in device ban was lifted |
created:device_management_group | info | Device group created |
updated:device_management_group | info | Device group updated |
deleted:device_management_group | warning | Device group deleted |
created:offsite_access | warning | Offsite window granted |
updated:offsite_access | warning | Offsite window updated |
deleted:offsite_access | warning | Offsite window revoked |
unlocked:locked_account | warning | Account unlocked (attempts cleared) |
logout:active_session | warning | All sessions terminated for a user |
deleted:active_session | warning | Single session terminated |
created:access_device | info | Device registered |
updated:access_device | info | Device updated |
deleted:access_device | warning | Device deleted |
rotated_secret:access_device | critical | Device API secret regenerated |
created:access_device_category | info | Category created |
updated:access_device_category | info | Category updated |
deleted:access_device_category | warning | Category deleted |
deleted:access_log | warning | Access log entry deleted |
banned:banned_device is rated critical). They appear in
Settings → System Logs under category Security, module Device Management.