For the complete documentation index, see llms.txt. This page is also available as Markdown.

Limited rights

Define ACL roles, controller permissions and per-role data filtering so that module-managed users see only what their role allows.

This recipe shows how a module gives the MikoPBX web interface role-based access control (RBAC): how to register custom ACL roles and rules, how to contribute per-controller permission flags consumed by the front-end, and how to inject per-role WHERE conditions into CDR queries so each role sees only its own call records.

Three Web-UI hooks carry the whole flow. They are declared on WebUIConfigInterface and CDRConfigInterface, and you override them in your module's config class:

Hook
Declared in
Purpose

onAfterACLPrepared(AclList $aclList)

WebUIConfigInterface

Add roles, components (controllers) and allow rules to the Phalcon ACL.

onGetControllerPermissions(string $controller, array &$permissions)

WebUIConfigInterface

Add custom permission flags for a controller, returned to JavaScript.

applyACLFiltersToCDRQuery(array &$parameters, array $sessionContext = [])

CDRConfigInterface

Add per-role WHERE conditions to CDR list queries.

The complete, production-grade implementation of this recipe is the ModuleUsersUI module. Read it in Extensions/ModuleUsersUI/Lib/ — the narrative below uses the fictional ModuleBlackList module (config class BlackListConf) for teaching, but every API it calls is taken from that real module and from the MikoPBX Core classes.

How MikoPBX resolves rights

MikoPBX uses Phalcon's in-memory ACL (Phalcon\Acl\Adapter\Memory, aliased as AclList). The ACL has three kinds of entries:

  • RolesPhalcon\Acl\Role. A logged-in session carries exactly one role.

  • ComponentsPhalcon\Acl\Component. In MikoPBX a component is a controller class name, e.g. MikoPBX\AdminCabinet\Controllers\CallDetailRecordsController.

  • Rulesallow($role, $component, $actions) grants a role the listed controller actions (index, modify, save, …).

The ACL is built in Core/src/Common/Providers/AclProvider.php and starts from setDefaultAction(AclEnum::DENY)anything not explicitly allowed is denied. Two roles are then registered before any module is consulted:

  • AclProvider::ROLE_ADMINS ('admins') gets allow('admins', '*', '*') — the administrator bypasses every check.

  • AclProvider::ROLE_GUESTS ('guests') gets deny('guests', '*', '*') — the anonymous default.

A module that introduces limited users defines additional roles and grants each one only the controllers and actions it should reach. When the user's session role is set to one of those custom roles, the SecurityPlugin denies every request that the ACL does not explicitly allow.

The ACL also grants a few things to '*' (every role) after the module hook runs: the Errors controller (show401/show404/show500), the Session controller (index/start/changeLanguage/end) and the stateless password helper endpoints. You cannot revoke those from a module hook — they are added after your onAfterACLPrepared() returns.

Step 1 — register roles and rules with onAfterACLPrepared

onAfterACLPrepared(AclList $aclList): void runs while MikoPBX builds the ACL (the result is then cached). Use it to add roles, components and allow rules. The signature is fixed by WebUIConfigInterface:

For ModuleBlackList we define one role per managed access group. Each role gets the controllers it is allowed to use; a group flagged fullAccess is granted everything with allow($role, '*', '*').

Key points, all verified against Extensions/ModuleUsersUI/Lib/UsersUIACL.php:

  • A component must be added (addComponent) before you can allow a role on it. ModuleUsersUI builds an $actionsArray[$controller] = [...actions] map and then loops addComponent(new Component($controller), $actions) followed by allow($role, $controller, $actions).

  • allow($role, '*', '*') is the real escape hatch ModuleUsersUI uses for its fullAccess groups.

  • Roles must be globally unique. ModuleUsersUI prefixes every role with UsersUIRoleID (constant Constants::MODULE_ROLE_PREFIX in Extensions/ModuleUsersUI/Lib/Constants.php); ModuleBlackList uses BlackListRoleID. This prefix is also how you recover the access-group id later (Step 3).

ModuleUsersUI additionally lets each enabled module contribute "always allowed" and "linked" controllers via optional static methods on a Modules\<Uniqid>\Lib\<Uniqid>ACL class (getAlwaysAllowed(), getAlwaysDenied(), getLinkedControllerActions()). Those are a ModuleUsersUI convention, not a Core hook — see UsersUIACL::addRulesFromModules() if you ship a module that should be governed by ModuleUsersUI's access groups.

Step 2 — expose permission flags with onGetControllerPermissions

The web front-end asks the Core AclController "what may the current user do on this controller?" so it can show or hide buttons. The Core computes the standard three-level flags itself (index, getNewRecords, modify, edit, save, delete, copy) from the SecurityPlugin, then lets modules add custom flags through this hook.

The contract, confirmed in Core/src/AdminCabinet/Controllers/AclController.php:

  • $controller is the fully-qualified controller class name (e.g. MikoPBX\AdminCabinet\Controllers\CallDetailRecordsController).

  • $permissions arrives as an empty array scoped to your module. Whatever you put in it is returned to JavaScript under data.custom. You do not touch the built-in flags — those are computed separately.

The relevant Core slice that calls this hook:

Step 3 — filter CDR data per role with applyACLFiltersToCDRQuery

A limited role that can open the Call Detail Records page should still see only its own calls. CDR list queries are funnelled through one hook before execution:

$parameters is the Phalcon query-builder array (conditions, bind) that is about to run. You mutate it in place to add your WHERE clause.

$sessionContext is filled from the caller's JWT by Core/src/PBXCoreREST/Controllers/BaseController.php (lines 368-387) and carries:

  • $sessionContext['role'] — the user's role,

  • $sessionContext['user_name'] — the login,

  • $sessionContext['session_id'] — the token/session id.

The hook is invoked from Core/src/PBXCoreREST/Lib/Cdr/GetListAction.php:

ModuleBlackList recovers the access-group id from the role prefix (the inverse of Step 1) and rewrites the query conditions:

The mutation pattern above is exactly what ModuleUsersUI does in Extensions/ModuleUsersUI/Lib/UsersUICDRFilter.php:

  • src_num/dst_num are the CDR table columns for source and destination extension numbers.

  • {allowedNumbers:array} is Phalcon's bound-array placeholder, paired with $parameters['bind']['allowedNumbers'].

  • ModuleUsersUI supports several modes (selected, outgoing-selected, not-selected, all) and falls back to conditions = '1=0' when a restricted group has no extensions — read its applyCDRFilterRules() for the full logic.

  • ModuleUsersUI splits the dispatch (UsersUIConf::applyACLFiltersToCDRQuery() reads $sessionContext['role'], then delegates to UsersUICDRFilter). Keep your config-class hook thin and put the query logic in a helper.

applyACLFiltersToCDRQuery only narrows the data set. The user must already be allowed to reach the CDR controller via Step 1; otherwise the page is denied before any query runs.

Note: REST RBAC with the ResourceSecurity attribute

The hooks above secure the web interface and CDR lists. For your module's REST controllers, MikoPBX 2025.1.1+ uses a resource-based attribute, MikoPBX\PBXCoreREST\Attributes\ResourceSecurity, applied to a controller class or an action method:

The constructor is ResourceSecurity(string $resource, ?ActionType $action = null, ?array $requirements = null, bool $optional = false, string $description = '', array $extensions = []). ActionType is one of READ, WRITE, ADMIN, SENSITIVE; SecurityType is one of LOCALHOST, BEARER_TOKEN, PUBLIC (Core/src/PBXCoreREST/Attributes/). For a real usage, see the Core controller that declares #[ResourceSecurity('employees', requirements: [SecurityType::LOCALHOST, SecurityType::BEARER_TOKEN])] in Core/src/PBXCoreREST/Controllers/Employees/RestController.php. Full REST RBAC details live in the REST API reference.

Where call-level isolation fits

ACL governs who can open which page and see which records. If you instead need to restrict which numbers a group may dial, that is enforced in the dialplan, not the ACL. The ModuleUsersGroups module (Extensions/ModuleUsersGroups/) is the reference for that: it builds group-isolation dialplan contexts and outbound-rule restrictions (Extensions/ModuleUsersGroups/Lib/UsersGroupsConf.php, model AllowedOutboundRules). It does not implement the ACL hooks described here — use it as the companion pattern when limited rights must extend to call routing.

Checklist

  1. Override onAfterACLPrepared() to add a uniquely-prefixed role per group, register each allowed controller as a component, and allow the actions (allow($role, '*', '*') for full access).

  2. Clear the ACL cache with AclProvider::clearCache() on enable/disable and whenever your role/rights models change.

  3. Override onGetControllerPermissions() to add custom UI flags under data.custom (UI hint only — never your sole guard).

  4. Override applyACLFiltersToCDRQuery() to add per-role CDR WHERE clauses; read the role from $sessionContext['role'] and return early when it is null.

  5. Guard your REST controllers with #[ResourceSecurity(...)].

See also

Last updated

Was this helpful?