diff --git a/customer_index.php b/customer_index.php index ee3bdbdd..70cb2ae3 100644 --- a/customer_index.php +++ b/customer_index.php @@ -30,6 +30,7 @@ use Froxlor\Api\Commands\Customers as Customers; use Froxlor\Cron\TaskId; use Froxlor\CurrentUser; use Froxlor\Database\Database; +use Froxlor\Database\DbManager; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Language; @@ -216,6 +217,22 @@ if ($page == 'overview') { Cronjob::inserttask(TaskId::REBUILD_VHOST); } + // Update global myqsl user password + if ($userinfo['mysqls'] != 0 && isset($_POST['change_global_mysql']) && $_POST['change_global_mysql'] == 'true') { + $allowed_mysqlservers = json_decode($userinfo['allowed_mysqlserver'] ?? '[]', true); + foreach ($allowed_mysqlservers as $dbserver) { + // require privileged access for target db-server + Database::needRoot(true, $dbserver, false); + // get DbManager + $dbm = new DbManager($log); + // give permission to the user on every access-host we have + foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { + $dbm->getManager()->grantPrivilegesTo($userinfo['loginname'], $new_password, $mysql_access_host, false, true); + } + $dbm->getManager()->flushPrivileges(); + } + } + Response::redirectTo($filename); } } elseif ($_POST['send'] == 'changetheme') { diff --git a/customer_mysql.php b/customer_mysql.php index 09f928d6..df044a64 100644 --- a/customer_mysql.php +++ b/customer_mysql.php @@ -30,8 +30,10 @@ use Froxlor\Api\Commands\Mysqls; use Froxlor\Api\Commands\MysqlServer; use Froxlor\CurrentUser; use Froxlor\Database\Database; +use Froxlor\Database\DbManager; use Froxlor\FroxlorLogger; use Froxlor\Settings; +use Froxlor\System\Crypt; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; @@ -74,6 +76,18 @@ if ($page == 'overview' || $page == 'mysqls') { ]; } + $view = 'user/table.html.twig'; + if ($collection->count() > 0) { + $view = 'user/table-note.html.twig'; + + $actions_links[] = [ + 'href' => $linker->getLink(['section' => 'mysql', 'page' => 'mysqls', 'action' => 'global_user']), + 'label' => lng('mysql.edit_global_user'), + 'icon' => 'fa-solid fa-user-tie', + 'class' => 'btn-outline-secondary' + ]; + } + $actions_links[] = [ 'href' => \Froxlor\Froxlor::DOCS_URL . 'user-guide/databases/', 'target' => '_blank', @@ -81,10 +95,13 @@ if ($page == 'overview' || $page == 'mysqls') { 'class' => 'btn-outline-secondary' ]; - UI::view('user/table.html.twig', [ + UI::view($view, [ 'listing' => Listing::format($collection, $mysql_list_data, 'mysql_list'), 'actions_links' => $actions_links, - 'entity_info' => lng('mysql.description') + 'entity_info' => lng('mysql.description'), + // alert-box + 'type' => 'info', + 'alert_msg' => lng('mysql.globaluserinfo', [$userinfo['loginname']]), ]); } elseif ($action == 'delete' && $id != 0) { try { @@ -199,5 +216,45 @@ if ($page == 'overview' || $page == 'mysqls') { ]); } } + } elseif ($action == 'global_user') { + + $allowed_mysqlservers = json_decode($userinfo['allowed_mysqlserver'] ?? '[]', true); + if ($userinfo['mysqls'] == 0 || empty($allowed_mysqlservers)) { + Response::dynamicError('No permission'); + } + + if (isset($_POST['send']) && $_POST['send'] == 'send') { + + $new_password = Crypt::validatePassword($_POST['mysql_password']); + foreach ($allowed_mysqlservers as $dbserver) { + // require privileged access for target db-server + Database::needRoot(true, $dbserver, false); + // get DbManager + $dbm = new DbManager($log); + // give permission to the user on every access-host we have + foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { + if ($dbm->getManager()->userExistsOnHost($userinfo['loginname'], $mysql_access_host)) { + // update password + $dbm->getManager()->grantPrivilegesTo($userinfo['loginname'], $new_password, $mysql_access_host, false, true, true); + } else { + // create missing user + $dbm->getManager()->grantPrivilegesTo($userinfo['loginname'], $new_password, $mysql_access_host, false, false, true); + } + } + $dbm->getManager()->flushPrivileges(); + } + + Response::redirectTo($filename, [ + 'page' => 'overview' + ]); + } else { + $mysql_global_user_data = include_once dirname(__FILE__) . '/lib/formfields/customer/mysql/formfield.mysql_global_user.php'; + + UI::view('user/form.html.twig', [ + 'formaction' => $linker->getLink(['section' => 'mysql', 'page' => 'mysqls', 'action' => 'global_user']), + 'formdata' => $mysql_global_user_data['mysql_global_user'], + 'editid' => $id + ]); + } } } diff --git a/lib/Froxlor/Api/Commands/Customers.php b/lib/Froxlor/Api/Commands/Customers.php index 97411b83..40ffe5d9 100644 --- a/lib/Froxlor/Api/Commands/Customers.php +++ b/lib/Froxlor/Api/Commands/Customers.php @@ -739,6 +739,21 @@ class Customers extends ApiCommand implements ResourceEntity } } + // Create default mysql-user if enabled + if ($mysqls != 0) { + foreach ($allowed_mysqlserver as $dbserver) { + // require privileged access for target db-server + Database::needRoot(true, $dbserver, false); + // get DbManager + $dbm = new DbManager($this->logger()); + // give permission to the user on every access-host we have + foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { + $dbm->getManager()->grantPrivilegesTo($loginname, $password, $mysql_access_host, false, false); + } + $dbm->getManager()->flushPrivileges(); + } + } + if ($sendpassword == '1') { $srv_hostname = Settings::Get('system.hostname'); if (Settings::Get('system.froxlordirectlyviahostname') == '0') { @@ -1297,12 +1312,32 @@ class Customers extends ApiCommand implements ResourceEntity ]); $upd_stmt = Database::prepare(" - UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `deactivated`= :deactivated WHERE `customerid` = :customerid"); + UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `deactivated`= :deactivated WHERE `customerid` = :customerid + "); Database::pexecute($upd_stmt, [ 'deactivated' => $deactivated, 'customerid' => $id ]); + // enable/disable global mysql-user (loginname) + foreach ($result['allowed_mysqlserver'] as $dbserver) { + // require privileged access for target db-server + Database::needRoot(true, $dbserver, false); + // get DbManager + $dbm = new DbManager($this->logger()); + foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { + // Prevent access, if deactivated + if ($deactivated) { + // failsafe if user has been deleted manually (requires MySQL 4.1.2+) + $dbm->getManager()->disableUser($result['loginname'], $mysql_access_host); + } else { + // Otherwise grant access + $dbm->getManager()->enableUser($result['loginname'], $mysql_access_host, true); + } + } + $dbm->getManager()->flushPrivileges(); + } + // Retrieve customer's databases $databases_stmt = Database::prepare("SELECT * FROM " . TABLE_PANEL_DATABASES . " WHERE customerid = :customerid ORDER BY `dbserver`"); Database::pexecute($databases_stmt, [ @@ -1323,9 +1358,7 @@ class Customers extends ApiCommand implements ResourceEntity $last_dbserver = $row_database['dbserver']; } - foreach (array_unique(explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { - $mysql_access_host = trim($mysql_access_host); - + foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { // Prevent access, if deactivated if ($deactivated) { // failsafe if user has been deleted manually (requires MySQL 4.1.2+) @@ -1616,6 +1649,19 @@ class Customers extends ApiCommand implements ResourceEntity ]); $id = $result['customerid']; + // remove global mysql-user (loginname) + foreach ($result['allowed_mysqlserver'] as $dbserver) { + // require privileged access for target db-server + Database::needRoot(true, $dbserver, false); + // get DbManager + $dbm = new DbManager($this->logger()); + foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { + $dbm->getManager()->deleteUser($result['loginname'], $mysql_access_host); + } + $dbm->getManager()->flushPrivileges(); + } + + // remove all databases $databases_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :id ORDER BY `dbserver` @@ -1631,8 +1677,8 @@ class Customers extends ApiCommand implements ResourceEntity $priv_changed = false; while ($row_database = $databases_stmt->fetch(PDO::FETCH_ASSOC)) { if ($last_dbserver != $row_database['dbserver']) { - Database::needRoot(true, $row_database['dbserver']); $dbm->getManager()->flushPrivileges(); + Database::needRoot(true, $row_database['dbserver']); $last_dbserver = $row_database['dbserver']; } $dbm->getManager()->deleteDatabase($row_database['databasename']); diff --git a/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php index 1edd99ee..ca6b94ee 100644 --- a/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php +++ b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php @@ -321,6 +321,7 @@ EOC; WHERE dom.`customerid` = cust.`customerid` AND cust.deactivated = 0 + AND dom.`ssl_enabled` = 1 AND dom.`letsencrypt` = 1 AND dom.`aliasdomain` IS NULL AND dom.`iswildcarddomain` = 0 @@ -382,6 +383,7 @@ EOC; WHERE dom.`customerid` = cust.`customerid` AND cust.deactivated = 0 + AND dom.`ssl_enabled` = 1 AND dom.`letsencrypt` = 1 AND dom.`aliasdomain` IS NULL AND dom.`iswildcarddomain` = 0 diff --git a/lib/Froxlor/Cron/Traffic/TrafficCron.php b/lib/Froxlor/Cron/Traffic/TrafficCron.php index 3d974c9d..f797b383 100644 --- a/lib/Froxlor/Cron/Traffic/TrafficCron.php +++ b/lib/Froxlor/Cron/Traffic/TrafficCron.php @@ -122,7 +122,7 @@ class TrafficCron extends FroxlorCron if ($mysql_usage_row) { $mysqlusage_all[$row_database['customerid']] += floatval($mysql_usage_row['customerusage']); } else { - FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Cannot get usage for database " . $row_database['databasename'] . "."); + FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Cannot get usage for database " . $row_database['databasename'] . "."); } } else { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Seems like the database " . $row_database['databasename'] . " had been removed manually."); diff --git a/lib/Froxlor/Database/Manager/DbManagerMySQL.php b/lib/Froxlor/Database/Manager/DbManagerMySQL.php index 470daa87..77e59495 100644 --- a/lib/Froxlor/Database/Manager/DbManagerMySQL.php +++ b/lib/Froxlor/Database/Manager/DbManagerMySQL.php @@ -76,9 +76,11 @@ class DbManagerMySQL * optional, whether the password is encrypted or not, default false * @param bool $update * optional, whether to update the password only (not create user) + * @param bool $grant_access_prefix + * optional, whether the given user will have access to all databases starting with the username, default false * @throws \Exception */ - public function grantPrivilegesTo(string $username, $password, string $access_host = null, bool $p_encrypted = false, bool $update = false) + public function grantPrivilegesTo(string $username, $password, string $access_host = null, bool $p_encrypted = false, bool $update = false, bool $grant_access_prefix = false) { $pwd_plugin = 'mysql_native_password'; if (is_array($password) && count($password) == 2) { @@ -108,7 +110,7 @@ class DbManagerMySQL ]); // grant privileges $stmt = Database::prepare(" - GRANT ALL ON `" . $username . "`.* TO :username@:host + GRANT ALL ON `" . $username . ($grant_access_prefix ? '%' : '') . "`.* TO :username@:host "); Database::pexecute($stmt, [ "username" => $username, @@ -219,17 +221,31 @@ class DbManagerMySQL * * @param string $username * @param string $host + * @param bool $grant_access_prefix * @throws \Exception */ - public function enableUser(string $username, string $host) + public function enableUser(string $username, string $host, bool $grant_access_prefix = false) { // check whether user exists to avoid errors + if ($this->userExistsOnHost($username, $host)) { + Database::query('GRANT ALL PRIVILEGES ON `' . $username . ($grant_access_prefix ? '%' : '') . '`.* TO `' . $username . '`@`' . $host . '`'); + Database::query('GRANT ALL PRIVILEGES ON `' . str_replace('_', '\_', $username) . ($grant_access_prefix ? '%' : '') . '` . * TO `' . $username . '`@`' . $host . '`'); + } + } + + /** + * Check whether a given username exists for the given host + * + * @param string $username + * @param string $host + * @return bool + * @throws \Exception + */ + public function userExistsOnHost(string $username, string $host): bool + { $exist_check_stmt = Database::prepare("SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '" . $username . "' AND host = '" . $host . "')"); $exist_check = Database::pexecute_first($exist_check_stmt); - if ($exist_check && array_pop($exist_check) == '1') { - Database::query('GRANT ALL PRIVILEGES ON `' . $username . '`.* TO `' . $username . '`@`' . $host . '`'); - Database::query('GRANT ALL PRIVILEGES ON `' . str_replace('_', '\_', $username) . '` . * TO `' . $username . '`@`' . $host . '`'); - } + return ($exist_check && array_pop($exist_check) == '1'); } /** diff --git a/lib/formfields/customer/mysql/formfield.mysql_global_user.php b/lib/formfields/customer/mysql/formfield.mysql_global_user.php new file mode 100644 index 00000000..87fcac80 --- /dev/null +++ b/lib/formfields/customer/mysql/formfield.mysql_global_user.php @@ -0,0 +1,53 @@ + (2010-) + * @license GPLv2 https://files.froxlor.org/misc/COPYING.txt + * @package Formfields + */ + +return [ + 'mysql_global_user' => [ + 'title' => lng('mysql.edit_global_user'), + 'self_overview' => ['section' => 'mysql', 'page' => 'mysqls'], + 'sections' => [ + 'section_a' => [ + 'title' => lng('mysql.edit_global_user'), + 'fields' => [ + 'username' => [ + 'label' => lng('login.username'), + 'value' => $userinfo['loginname'], + 'type' => 'text', + 'readonly' => true + ], + 'mysql_password' => [ + 'label' => lng('login.password'), + 'type' => 'password', + 'autocomplete' => 'off', + 'mandatory' => true, + 'next_to' => [ + 'mysql_password_suggestion' => [ + 'next_to_prefix' => lng('customer.generated_pwd') . ':', + 'type' => 'text', + 'visible' => (Settings::Get('panel.password_regex') == ''), + 'value' => Crypt::generatePassword(), + 'readonly' => true + ] + ] + ] + ] + ] + ] + ] +]; diff --git a/lng/de.lng.php b/lng/de.lng.php index 61e6689a..eac909a1 100644 --- a/lng/de.lng.php +++ b/lng/de.lng.php @@ -519,6 +519,7 @@ return [ 'new_password_ifnotempty' => 'Neues Passwort (leer für keine Änderung)', 'also_change_ftp' => 'Auch Passwort des Haupt-FTP-Zugangs ändern', 'also_change_stats' => ' Auch Passwort der Statistikseite ändern', + 'also_change_global_mysql' => 'Auch Passwort des globalen MySQL-Zugangs ändern', ], 'cron' => [ 'cronname' => 'Cronjob-Name', @@ -1143,7 +1144,9 @@ Vielen Dank, Ihr Administrator', 'privileged_passwd' => 'Passwort für privilegierten Benutzer', 'unprivileged_passwd' => 'Passwort für nicht privilegierten Benutzer', 'mysql_ssl_ca_file' => 'SSL-Serverzertifikat', - 'mysql_ssl_verify_server_certificate' => 'Verifizieren des SSL-Serverzertifikats' + 'mysql_ssl_verify_server_certificate' => 'Verifizieren des SSL-Serverzertifikats', + 'globaluserinfo' => 'Um auf Datenbanken zuzugreifen, kann zusätzlich der Froxlor-Login (Benutzer: %s) verwendet werden, dieser hat automatisch Zugriff auf alle Datenbanken.
Es wird empfohlen diesen nicht für Applikationen zu nutzen, lediglich zur Administration (z.B. via phpMyAdmin).', + 'edit_global_user' => 'Admin Benutzer bearbeiten', ], 'panel' => [ 'edit' => 'bearbeiten', diff --git a/lng/en.lng.php b/lng/en.lng.php index eef66d37..272511fe 100644 --- a/lng/en.lng.php +++ b/lng/en.lng.php @@ -566,6 +566,7 @@ return [ 'new_password_ifnotempty' => 'New password (empty = no change)', 'also_change_ftp' => ' also change password of the main FTP account', 'also_change_stats' => ' also change password for the statistics page', + 'also_change_global_mysql' => 'also change password for global MySQL account', ], 'cron' => [ 'cronname' => 'cronjob-name', @@ -1215,7 +1216,9 @@ Yours sincerely, your administrator', 'privileged_passwd' => 'Password for privileged user', 'unprivileged_passwd' => 'Password for unprivileged user', 'mysql_ssl_ca_file' => 'SSL server certificate', - 'mysql_ssl_verify_server_certificate' => 'Verify SSL server certificate' + 'mysql_ssl_verify_server_certificate' => 'Verify SSL server certificate', + 'globaluserinfo' => 'To access your databases, you can additionally use your froxlor login (user: %s) which automatically has access to all your databases.
It is recommended not to use this for applications, only for administration (e.g. via phpMyAdmin).', + 'edit_global_user' => 'Edit admin user', ], 'opcacheinfo' => [ 'generaltitle' => 'General Information', diff --git a/templates/Froxlor/user/profile.html.twig b/templates/Froxlor/user/profile.html.twig index 670a26f8..d0ded110 100644 --- a/templates/Froxlor/user/profile.html.twig +++ b/templates/Froxlor/user/profile.html.twig @@ -57,6 +57,18 @@ + {% if userinfo.mysqls != 0 %} +
+ +
+ + +
+
+ {% endif %} + {% endif %}