feat: wire ACL checks into all controller actions and views

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) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-06-02 15:33:33 -05:00
parent e69953ad17
commit 33ce8b115c
@@ -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')]);
}
}