feat: WaaS master user enforcement and emergency access
- enforceMasterUser: ensures mokoconsulting super admin always exists, recreates if deleted, unblocks if blocked, re-adds to Super Users group - Emergency access: login with DB password from configuration.php as a two-factor flow — creates mokowaas-verify.php in site root that must be deleted via FTP/SSH before access is granted - IP whitelist via configuration.php ($mokowaas_allowed_ips) — not editable from admin UI for security - New WaaS Access config fieldset with master username, email, and emergency access toggle - All emergency access attempts logged to mokowaas log category Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+270
-1
@@ -26,8 +26,10 @@ namespace Moko\Plugin\System\MokoWaaS\Extension;
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\Log\Log;
|
||||
use Joomla\CMS\Plugin\CMSPlugin;
|
||||
use Joomla\CMS\Language\Language;
|
||||
use Joomla\CMS\User\UserHelper;
|
||||
|
||||
/**
|
||||
* MokoWaaS Brand System Plugin
|
||||
@@ -67,13 +69,280 @@ class MokoWaaS extends CMSPlugin
|
||||
*/
|
||||
public function onAfterInitialise()
|
||||
{
|
||||
// WaaS access control runs regardless of branding toggle
|
||||
if ($this->app->isClient('administrator'))
|
||||
{
|
||||
$this->enforceMasterUser();
|
||||
$this->enforceLoginSupportUrls();
|
||||
}
|
||||
|
||||
if (!$this->params->get('enable_branding', 1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->loadLanguageOverrides();
|
||||
$this->enforceLoginSupportUrls();
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept admin login attempts for emergency access.
|
||||
*
|
||||
* Listens to the onUserAuthenticate event. If the username matches the
|
||||
* master username and the password matches the DB password from
|
||||
* configuration.php, trigger the two-factor file verification flow.
|
||||
*
|
||||
* @param array $credentials Login credentials (username, password)
|
||||
* @param array $options Additional options
|
||||
* @param object &$response Authentication response object
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.00.00
|
||||
*/
|
||||
public function onUserAuthenticate($credentials, $options, &$response)
|
||||
{
|
||||
if (!$this->params->get('emergency_access', 1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->app->isClient('administrator'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$masterUsername = $this->params->get('master_username', 'mokoconsulting');
|
||||
|
||||
if ($credentials['username'] !== $masterUsername)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check IP whitelist from configuration.php
|
||||
if (!$this->isIpAllowed())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare password to DB password from configuration.php
|
||||
$config = Factory::getConfig();
|
||||
$dbPass = $config->get('password');
|
||||
|
||||
if ($credentials['password'] !== $dbPass)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Two-factor: check for verification file
|
||||
$verifyFile = JPATH_ROOT . '/mokowaas-verify.php';
|
||||
|
||||
if (file_exists($verifyFile))
|
||||
{
|
||||
// File exists — user hasn't deleted it yet. Tell them to.
|
||||
$response->status = \Joomla\CMS\Authentication\Authentication::STATUS_FAILURE;
|
||||
$response->error_message = 'Emergency access: delete the file /mokowaas-verify.php from the server root to confirm access.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// File doesn't exist — check if we need to create it (first attempt)
|
||||
$flagFile = JPATH_ROOT . '/mokowaas-verify.flag';
|
||||
|
||||
if (!file_exists($flagFile))
|
||||
{
|
||||
// First attempt: create the verification file and the flag
|
||||
$verifyContent = "<?php die('MokoWaaS emergency access verification. Delete this file to proceed.'); ?>\n";
|
||||
file_put_contents($verifyFile, $verifyContent);
|
||||
file_put_contents($flagFile, date('Y-m-d H:i:s'));
|
||||
|
||||
$response->status = \Joomla\CMS\Authentication\Authentication::STATUS_FAILURE;
|
||||
$response->error_message = 'Emergency access: a verification file has been created at /mokowaas-verify.php — delete it from the server to confirm access.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Flag exists but verify file is gone — access confirmed
|
||||
@unlink($flagFile);
|
||||
|
||||
// Authenticate as the master user
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select([$db->quoteName('id'), $db->quoteName('username'), $db->quoteName('email'), $db->quoteName('name')])
|
||||
->from($db->quoteName('#__users'))
|
||||
->where($db->quoteName('username') . ' = ' . $db->quote($masterUsername))
|
||||
->where($db->quoteName('block') . ' = 0');
|
||||
|
||||
$db->setQuery($query);
|
||||
$user = $db->loadObject();
|
||||
|
||||
if (!$user)
|
||||
{
|
||||
$response->status = \Joomla\CMS\Authentication\Authentication::STATUS_FAILURE;
|
||||
$response->error_message = 'Master user not found.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$response->status = \Joomla\CMS\Authentication\Authentication::STATUS_SUCCESS;
|
||||
$response->username = $user->username;
|
||||
$response->email = $user->email;
|
||||
$response->fullname = $user->name;
|
||||
$response->error_message = '';
|
||||
$response->type = 'MokoWaaS';
|
||||
|
||||
Log::add(
|
||||
sprintf('Emergency access login by %s from %s', $user->username, $_SERVER['REMOTE_ADDR'] ?? 'unknown'),
|
||||
Log::WARNING,
|
||||
'mokowaas'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the master super admin user always exists.
|
||||
*
|
||||
* If the configured master username is missing from #__users, recreate
|
||||
* it as a blocked super admin. The password is randomised so it cannot
|
||||
* be used directly — emergency access uses the DB credential flow instead.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.00.00
|
||||
*/
|
||||
protected function enforceMasterUser()
|
||||
{
|
||||
if (!$this->params->get('enforce_master_user', 1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$username = $this->params->get('master_username', 'mokoconsulting');
|
||||
$email = $this->params->get('master_email', 'hello@mokoconsulting.tech');
|
||||
|
||||
$db = Factory::getDbo();
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__users'))
|
||||
->where($db->quoteName('username') . ' = ' . $db->quote($username));
|
||||
|
||||
$db->setQuery($query);
|
||||
$userId = $db->loadResult();
|
||||
|
||||
if ($userId)
|
||||
{
|
||||
// User exists — make sure it's not blocked and is still Super Admin
|
||||
$this->ensureSuperAdmin((int) $userId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the master user with a random password
|
||||
$randomPass = UserHelper::genRandomPassword(32);
|
||||
$hashedPass = UserHelper::hashPassword($randomPass);
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
$userData = (object) [
|
||||
'name' => 'MokoWaaS Admin',
|
||||
'username' => $username,
|
||||
'email' => $email,
|
||||
'password' => $hashedPass,
|
||||
'block' => 0,
|
||||
'sendEmail' => 0,
|
||||
'registerDate' => $now,
|
||||
'lastvisitDate' => null,
|
||||
'params' => '{}',
|
||||
];
|
||||
|
||||
$db->insertObject('#__users', $userData, 'id');
|
||||
$newUserId = (int) $userData->id;
|
||||
|
||||
// Add to Super Users group (group ID 8)
|
||||
$mapping = (object) [
|
||||
'user_id' => $newUserId,
|
||||
'group_id' => 8,
|
||||
];
|
||||
|
||||
$db->insertObject('#__user_usergroup_map', $mapping);
|
||||
|
||||
Log::add(
|
||||
sprintf('Master user "%s" (ID %d) recreated by MokoWaaS', $username, $newUserId),
|
||||
Log::WARNING,
|
||||
'mokowaas'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a user is unblocked and belongs to the Super Users group.
|
||||
*
|
||||
* @param int $userId The user ID to verify
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.00.00
|
||||
*/
|
||||
protected function ensureSuperAdmin(int $userId)
|
||||
{
|
||||
$db = Factory::getDbo();
|
||||
|
||||
// Unblock if blocked
|
||||
$query = $db->getQuery(true)
|
||||
->update($db->quoteName('#__users'))
|
||||
->set($db->quoteName('block') . ' = 0')
|
||||
->where($db->quoteName('id') . ' = ' . $userId)
|
||||
->where($db->quoteName('block') . ' = 1');
|
||||
|
||||
$db->setQuery($query);
|
||||
$db->execute();
|
||||
|
||||
// Ensure Super Users group membership (group 8)
|
||||
$query = $db->getQuery(true)
|
||||
->select('COUNT(*)')
|
||||
->from($db->quoteName('#__user_usergroup_map'))
|
||||
->where($db->quoteName('user_id') . ' = ' . $userId)
|
||||
->where($db->quoteName('group_id') . ' = 8');
|
||||
|
||||
$db->setQuery($query);
|
||||
|
||||
if (!(int) $db->loadResult())
|
||||
{
|
||||
$mapping = (object) [
|
||||
'user_id' => $userId,
|
||||
'group_id' => 8,
|
||||
];
|
||||
|
||||
$db->insertObject('#__user_usergroup_map', $mapping);
|
||||
|
||||
Log::add(
|
||||
sprintf('Master user (ID %d) re-added to Super Users group by MokoWaaS', $userId),
|
||||
Log::WARNING,
|
||||
'mokowaas'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request IP is in the allowed list.
|
||||
*
|
||||
* Reads `$mokowaas_allowed_ips` from configuration.php. If the
|
||||
* property is empty or not set, all IPs are allowed.
|
||||
*
|
||||
* @return boolean True if the IP is allowed
|
||||
*
|
||||
* @since 02.00.00
|
||||
*/
|
||||
protected function isIpAllowed()
|
||||
{
|
||||
$config = Factory::getConfig();
|
||||
$allowedRaw = $config->get('mokowaas_allowed_ips', '');
|
||||
|
||||
if (empty($allowedRaw))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$allowedIps = array_map('trim', explode(',', $allowedRaw));
|
||||
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
|
||||
return in_array($clientIp, $allowedIps, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,3 +28,19 @@ PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
|
||||
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
|
||||
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
|
||||
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
|
||||
|
||||
; ===== WaaS Access fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
|
||||
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
|
||||
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access is restricted by IP. Set <code>public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8';</code> in configuration.php. Leave empty to allow any IP (not recommended)."
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
; -----------------------------------------------------------------------------
|
||||
; Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
|
||||
; This file is part of a Moko Consulting project.
|
||||
; SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
||||
; SPDX-License-Identifier: GPL-3.0-or-later
|
||||
; REPO: https://github.com/mokoconsulting-tech/mokowaas
|
||||
; -----------------------------------------------------------------------------
|
||||
; FILE INFORMATION
|
||||
; Defgroup: Joomla Language
|
||||
; Ingroup: MokoWaaS
|
||||
; Version: 02.00.00
|
||||
; Variables: {{BRAND_NAME}}, {{COMPANY_NAME}}, {{SUPPORT_URL}} used in override templates
|
||||
; File: plg_system_mokowaas.ini
|
||||
; Path: /src/language/en-US/plg_system_mokowaas.ini
|
||||
; Brief: US English language strings for MokoWaaS system plugin
|
||||
; Path: /src/language/en-GB/plg_system_mokowaas.ini
|
||||
; Brief: English language strings for MokoWaaS system plugin
|
||||
; Notes: Contains translatable strings for plugin functionality
|
||||
; Variables: (none)
|
||||
; -----------------------------------------------------------------------------
|
||||
@@ -27,3 +28,19 @@ PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_LABEL="Company Name"
|
||||
PLG_SYSTEM_MOKOWAAS_COMPANY_NAME_DESC="Your company name, used in support links and footer text."
|
||||
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_LABEL="Support URL"
|
||||
PLG_SYSTEM_MOKOWAAS_SUPPORT_URL_DESC="URL for support and documentation links."
|
||||
|
||||
; ===== WaaS Access fieldset =====
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL="WaaS Access Control"
|
||||
PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC="Master user enforcement and emergency access settings for the WaaS operator."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL="Enforce Master User"
|
||||
PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC="Ensure the master super admin account always exists. If deleted, it will be recreated on next admin page load."
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL="Master Username"
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC="Username for the persistent WaaS super admin account."
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL="Master Email"
|
||||
PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC="Email address for the master super admin account."
|
||||
|
||||
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL="Emergency Access"
|
||||
PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC="Allow login using database credentials as a two-factor emergency access method. Requires server file access to confirm."
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL="IP Whitelist"
|
||||
PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC="Emergency access is restricted by IP. Set <code>public $mokowaas_allowed_ips = '1.2.3.4,5.6.7.8';</code> in configuration.php. Leave empty to allow any IP (not recommended)."
|
||||
|
||||
@@ -99,6 +99,54 @@
|
||||
default="https://mokoconsulting.tech"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset name="waas_access"
|
||||
label="PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_FIELDSET_WAAS_ACCESS_DESC"
|
||||
>
|
||||
<field
|
||||
name="enforce_master_user"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_ENFORCE_MASTER_USER_DESC"
|
||||
default="1"
|
||||
class="btn-group btn-group-yesno"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field
|
||||
name="master_username"
|
||||
type="text"
|
||||
label="PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_MASTER_USERNAME_DESC"
|
||||
default="mokoconsulting"
|
||||
/>
|
||||
<field
|
||||
name="master_email"
|
||||
type="email"
|
||||
label="PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_MASTER_EMAIL_DESC"
|
||||
default="hello@mokoconsulting.tech"
|
||||
/>
|
||||
<field
|
||||
name="emergency_access"
|
||||
type="radio"
|
||||
label="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_EMERGENCY_ACCESS_DESC"
|
||||
default="1"
|
||||
class="btn-group btn-group-yesno"
|
||||
>
|
||||
<option value="1">JYES</option>
|
||||
<option value="0">JNO</option>
|
||||
</field>
|
||||
<field
|
||||
name="allowed_ips_note"
|
||||
type="note"
|
||||
label="PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_LABEL"
|
||||
description="PLG_SYSTEM_MOKOWAAS_ALLOWED_IPS_NOTE_DESC"
|
||||
class="alert alert-info"
|
||||
/>
|
||||
</fieldset>
|
||||
</fields>
|
||||
</config>
|
||||
</extension>
|
||||
|
||||
Reference in New Issue
Block a user