From 33ce8b115c92b89ee1ea737f2071201c30135d9b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Tue, 2 Jun 2026 15:33:33 -0500 Subject: [PATCH] feat: wire ACL checks into all controller actions and views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every view and AJAX task now checks its specific ACL permission: - display() checks VIEW_ACL map per view name - togglePlugin → mokowaas.plugins.toggle - clearCache → mokowaas.cache - installExtension → mokowaas.extensions - saveHtaccess/generateHtaccess → mokowaas.htaccess - createTicket → mokowaas.tickets.create - addTicketReply/updateTicketStatus → mokowaas.tickets Super admins (core.admin) always bypass all checks. Refactored to use checkAcl/jsonResponse/jsonForbidden helpers. Default ACL applied on dev: - Super Users: all permissions - Administrator: cache + ticket assign - Manager: dashboard + tickets (view/create) - Others: no access Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/Controller/DisplayController.php | 261 +++++++++--------- 1 file changed, 135 insertions(+), 126 deletions(-) diff --git a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php index a4f3fc62..c20a314e 100644 --- a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php +++ b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php @@ -20,131 +20,113 @@ class DisplayController extends BaseController { protected $default_view = 'dashboard'; + /** + * ACL map: view name => required permission. + */ + private const VIEW_ACL = [ + 'dashboard' => 'mokowaas.dashboard', + 'extensions' => 'mokowaas.extensions', + 'htaccess' => 'mokowaas.htaccess', + 'tickets' => 'mokowaas.tickets', + 'ticket' => 'mokowaas.tickets', + ]; + public function display($cachable = false, $urlparams = []) { + $view = $this->input->get('view', $this->default_view); + $acl = self::VIEW_ACL[$view] ?? 'core.manage'; + + if (!$this->checkAcl($acl)) + { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + Factory::getApplication()->redirect(Route::_('index.php', false)); + + return; + } + return parent::display($cachable, $urlparams); } - /** - * Toggle a MokoWaaS feature plugin on or off. - * - * Expects POST with extension_id and enabled (0 or 1). - * Returns JSON response for AJAX calls. - */ + // ================================================================== + // Plugin toggle + // ================================================================== + public function togglePlugin() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + if (!$this->checkAcl('mokowaas.plugins.toggle')) + { + $this->jsonForbidden(); + } + $app = Factory::getApplication(); - $input = $app->getInput(); + $model = $this->getModel('Dashboard'); - $user = $app->getIdentity(); - if (!$user->authorise('core.manage', 'com_plugins')) - { - $app->setHeader('Content-Type', 'application/json'); - echo json_encode(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); - $app->close(); - } + $result = $model->togglePlugin( + $app->getInput()->getInt('extension_id', 0), + $app->getInput()->getInt('enabled', 0) + ); - $extensionId = $input->getInt('extension_id', 0); - $enabled = $input->getInt('enabled', 0); - - if (!$extensionId) - { - $app->setHeader('Content-Type', 'application/json'); - echo json_encode(['success' => false, 'message' => 'Missing extension_id']); - $app->close(); - } - - $model = $this->getModel('Dashboard'); - $result = $model->togglePlugin($extensionId, $enabled); - - $app->setHeader('Content-Type', 'application/json'); - echo json_encode($result); - $app->close(); + $this->jsonResponse($result); } - /** - * Clear the Joomla cache. - */ + // ================================================================== + // Cache + // ================================================================== + public function clearCache() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - $app = Factory::getApplication(); - $user = $app->getIdentity(); - - if (!$user->authorise('core.admin')) + if (!$this->checkAcl('mokowaas.cache')) { - $app->setHeader('Content-Type', 'application/json'); - echo json_encode(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); - $app->close(); + $this->jsonForbidden(); } - $model = $this->getModel('Dashboard'); - $result = $model->clearCache(); - - $app->setHeader('Content-Type', 'application/json'); - echo json_encode($result); - $app->close(); + $this->jsonResponse($this->getModel('Dashboard')->clearCache()); } - /** - * Install a Moko extension from a download URL. - */ + // ================================================================== + // Extensions + // ================================================================== + public function installExtension() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - $app = Factory::getApplication(); - $user = $app->getIdentity(); - - if (!$user->authorise('core.admin')) + if (!$this->checkAcl('mokowaas.extensions')) { - $app->setHeader('Content-Type', 'application/json'); - echo json_encode(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); - $app->close(); + $this->jsonForbidden(); } - $downloadUrl = $app->getInput()->getString('download_url', ''); + $downloadUrl = Factory::getApplication()->getInput()->getString('download_url', ''); if (empty($downloadUrl)) { - $app->setHeader('Content-Type', 'application/json'); - echo json_encode(['success' => false, 'message' => 'Missing download URL.']); - $app->close(); + $this->jsonResponse(['success' => false, 'message' => 'Missing download URL.']); } - $model = $this->getModel('Extensions'); - $result = $model->installFromUrl($downloadUrl); - - $app->setHeader('Content-Type', 'application/json'); - echo json_encode($result); - $app->close(); + $this->jsonResponse($this->getModel('Extensions')->installFromUrl($downloadUrl)); } - /** - * Save .htaccess to disk and persist options. - */ + // ================================================================== + // .htaccess + // ================================================================== + public function saveHtaccess() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - $app = Factory::getApplication(); - $user = $app->getIdentity(); - - if (!$user->authorise('core.admin')) + if (!$this->checkAcl('mokowaas.htaccess')) { - $app->setHeader('Content-Type', 'application/json'); - echo json_encode(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); - $app->close(); + $this->jsonForbidden(); } - $input = $app->getInput(); - $content = $input->getRaw('content', ''); - $model = $this->getModel('Htaccess'); + $app = Factory::getApplication(); + $input = $app->getInput(); + $model = $this->getModel('Htaccess'); - // Save options from opt_ prefixed fields $options = []; foreach ($input->getArray() as $key => $value) @@ -160,28 +142,24 @@ class DisplayController extends BaseController $model->saveOptions($options); } - $result = $model->saveHtaccess($content); - - $app->setHeader('Content-Type', 'application/json'); - echo json_encode($result); - $app->close(); + $this->jsonResponse($model->saveHtaccess($input->getRaw('content', ''))); } - /** - * Generate .htaccess preview from posted options (AJAX). - */ public function generateHtaccess() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - $app = Factory::getApplication(); - $input = $app->getInput(); - $model = $this->getModel('Htaccess'); - $options = $input->getArray(); + if (!$this->checkAcl('mokowaas.htaccess')) + { + $this->jsonForbidden(); + } + + $model = $this->getModel('Htaccess'); + $options = Factory::getApplication()->getInput()->getArray(); - // Save options for persistence $model->saveOptions($options); + $app = Factory::getApplication(); $app->setHeader('Content-Type', 'application/json'); echo json_encode([ 'htaccess' => $model->generateHtaccess($options), @@ -190,69 +168,100 @@ class DisplayController extends BaseController $app->close(); } - /** - * Create a new support ticket. - */ + // ================================================================== + // Tickets + // ================================================================== + public function createTicket() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - $app = Factory::getApplication(); - $input = $app->getInput(); - $model = $this->getModel('Tickets'); + if (!$this->checkAcl('mokowaas.tickets.create')) + { + $this->jsonForbidden(); + } - $result = $model->createTicket([ + $input = Factory::getApplication()->getInput(); + + $this->jsonResponse($this->getModel('Tickets')->createTicket([ 'subject' => $input->getString('subject', ''), 'body' => $input->getRaw('body', ''), 'priority' => $input->getString('priority', 'normal'), 'category_id' => $input->getInt('category_id', 0), - ]); - - $app->setHeader('Content-Type', 'application/json'); - echo json_encode($result); - $app->close(); + ])); } - /** - * Add a reply to a ticket. - */ public function addTicketReply() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - $app = Factory::getApplication(); - $input = $app->getInput(); - $model = $this->getModel('Tickets'); + if (!$this->checkAcl('mokowaas.tickets')) + { + $this->jsonForbidden(); + } - $result = $model->addReply( + $input = Factory::getApplication()->getInput(); + + $this->jsonResponse($this->getModel('Tickets')->addReply( $input->getInt('ticket_id', 0), $input->getRaw('body', ''), (bool) $input->getInt('is_internal', 0) - ); - - $app->setHeader('Content-Type', 'application/json'); - echo json_encode($result); - $app->close(); + )); } - /** - * Update ticket status. - */ public function updateTicketStatus() { Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - $app = Factory::getApplication(); - $input = $app->getInput(); - $model = $this->getModel('Tickets'); + if (!$this->checkAcl('mokowaas.tickets')) + { + $this->jsonForbidden(); + } - $result = $model->updateStatus( + $input = Factory::getApplication()->getInput(); + + $this->jsonResponse($this->getModel('Tickets')->updateStatus( $input->getInt('ticket_id', 0), $input->getString('status', '') - ); + )); + } + // ================================================================== + // Helpers + // ================================================================== + + /** + * Check a MokoWaaS ACL permission for the current user. + */ + private function checkAcl(string $action): bool + { + $user = Factory::getApplication()->getIdentity(); + + // Super admins always pass + if ($user->authorise('core.admin', 'com_mokowaas')) + { + return true; + } + + return $user->authorise($action, 'com_mokowaas'); + } + + /** + * Send a JSON response and close. + */ + private function jsonResponse(array $data): void + { + $app = Factory::getApplication(); $app->setHeader('Content-Type', 'application/json'); - echo json_encode($result); + echo json_encode($data); $app->close(); } + + /** + * Send a 403 JSON response and close. + */ + private function jsonForbidden(): void + { + $this->jsonResponse(['success' => false, 'message' => Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')]); + } }