From 079047b9feb95efc04d6b3b5bdb02c58e61fd152 Mon Sep 17 00:00:00 2001 From: Michael Kaufmann Date: Tue, 3 Dec 2024 17:01:28 +0100 Subject: [PATCH] fix permissions of global mysql-user for customers; fixes #1286 Signed-off-by: Michael Kaufmann --- customer_mysql.php | 2 +- install/froxlor.sql.php | 2 +- install/updates/froxlor/update_2.2.inc.php | 35 +++++++++ lib/Froxlor/Api/Commands/Customers.php | 2 +- lib/Froxlor/Api/Commands/Mysqls.php | 6 +- lib/Froxlor/Database/DbManager.php | 28 ++++++- .../Database/Manager/DbManagerMySQL.php | 78 ++++++++++++++++++- lib/Froxlor/Froxlor.php | 2 +- 8 files changed, 142 insertions(+), 13 deletions(-) diff --git a/customer_mysql.php b/customer_mysql.php index e2372448a8..2a28ef2502 100644 --- a/customer_mysql.php +++ b/customer_mysql.php @@ -228,7 +228,7 @@ $new_password = Crypt::validatePassword(Request::post('mysql_password')); foreach ($allowed_mysqlservers as $dbserver) { // require privileged access for target db-server - Database::needRoot(true, $dbserver, false); + Database::needRoot(true, $dbserver, true); // get DbManager $dbm = new DbManager($log); // give permission to the user on every access-host we have diff --git a/install/froxlor.sql.php b/install/froxlor.sql.php index 53d7be99f0..0c4a71e184 100644 --- a/install/froxlor.sql.php +++ b/install/froxlor.sql.php @@ -734,7 +734,7 @@ ('panel', 'settings_mode', '0'), ('panel', 'menu_collapsed', '1'), ('panel', 'version', '2.2.5'), - ('panel', 'db_version', '202411200'); + ('panel', 'db_version', '202412030'); DROP TABLE IF EXISTS `panel_tasks`; diff --git a/install/updates/froxlor/update_2.2.inc.php b/install/updates/froxlor/update_2.2.inc.php index 4efaf7973c..2c773ab13c 100644 --- a/install/updates/froxlor/update_2.2.inc.php +++ b/install/updates/froxlor/update_2.2.inc.php @@ -24,6 +24,7 @@ */ use Froxlor\Database\Database; +use Froxlor\Database\DbManager; use Froxlor\Froxlor; use Froxlor\Install\Update; use Froxlor\Settings; @@ -209,3 +210,37 @@ Froxlor::updateToDbVersion('202411200'); } + +if (Froxlor::isDatabaseVersion('202411200')) { + + Update::showUpdateStep("Adjusting customer mysql global user"); + // get all customers that are not deactivated and that have at least one database (hence a global database-user) + $customers = Database::query(" + SELECT DISTINCT c.loginname, c.allowed_mysqlserver + FROM `" . TABLE_PANEL_CUSTOMERS . "` c + LEFT JOIN `" . TABLE_PANEL_DATABASES . "` d ON c.customerid = d.customerid + WHERE c.deactivated = '0' AND d.id IS NOT NULL + "); + while ($customer = $customers->fetch(\PDO::FETCH_ASSOC)) { + $current_allowed_mysqlserver = !empty($customer['allowed_mysqlserver']) ? json_decode($customer['allowed_mysqlserver'], true) : []; + foreach ($current_allowed_mysqlserver as $dbserver) { + // require privileged access for target db-server + Database::needRoot(true, $dbserver, true); + // get DbManager + $dbm = new DbManager($this->logger()); + foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { + if ($dbm->getManager()->userExistsOnHost($customer['loginname'], $mysql_access_host)) { + // deactivate temporarily + $dbm->getManager()->disableUser($customer['loginname'], $mysql_access_host); + // re-enable + $dbm->getManager()->enableUser($customer['loginname'], $mysql_access_host, true); + } + } + $dbm->getManager()->flushPrivileges(); + Database::needRoot(false); + } + } + Update::lastStepStatus(0); + + Froxlor::updateToDbVersion('202412030'); +} diff --git a/lib/Froxlor/Api/Commands/Customers.php b/lib/Froxlor/Api/Commands/Customers.php index 3808dc6e39..0f193c2370 100644 --- a/lib/Froxlor/Api/Commands/Customers.php +++ b/lib/Froxlor/Api/Commands/Customers.php @@ -1347,7 +1347,7 @@ public function update() $current_allowed_mysqlserver = isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_mysqlserver'], true) : []; foreach ($current_allowed_mysqlserver as $dbserver) { // require privileged access for target db-server - Database::needRoot(true, $dbserver, false); + Database::needRoot(true, $dbserver, true); // get DbManager $dbm = new DbManager($this->logger()); foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { diff --git a/lib/Froxlor/Api/Commands/Mysqls.php b/lib/Froxlor/Api/Commands/Mysqls.php index dcebc412fe..377afdda3e 100644 --- a/lib/Froxlor/Api/Commands/Mysqls.php +++ b/lib/Froxlor/Api/Commands/Mysqls.php @@ -113,9 +113,9 @@ public function add() if (strlen($newdb_params['loginname'] . '_' . $databasename) > Database::getSqlUsernameLength()) { throw new Exception("Database name cannot be longer than " . (Database::getSqlUsernameLength() - strlen($newdb_params['loginname'] . '_')) . " characters.", 406); } - $username = $dbm->createDatabase($newdb_params['loginname'] . '_' . $databasename, $password, $dbserver); + $username = $dbm->createDatabase($newdb_params['loginname'] . '_' . $databasename, $password, $dbserver, 0, $newdb_params['loginname']); } else { - $username = $dbm->createDatabase($newdb_params['loginname'], $password, $dbserver, $newdb_params['mysql_lastaccountnumber']); + $username = $dbm->createDatabase($newdb_params['loginname'], $password, $dbserver, $newdb_params['mysql_lastaccountnumber'], $newdb_params['loginname']); } // we've checked against the password in dbm->createDatabase @@ -541,7 +541,7 @@ public function delete() // Begin root-session Database::needRoot(true, $result['dbserver'], false); $dbm = new DbManager($this->logger()); - $dbm->getManager()->deleteDatabase($result['databasename']); + $dbm->getManager()->deleteDatabase($result['databasename'], $customer['loginname']); Database::needRoot(false); // End root-session diff --git a/lib/Froxlor/Database/DbManager.php b/lib/Froxlor/Database/DbManager.php index 2fdae1e6b5..8ef72dab1e 100644 --- a/lib/Froxlor/Database/DbManager.php +++ b/lib/Froxlor/Database/DbManager.php @@ -102,8 +102,26 @@ public static function correctMysqlUsers(array $mysql_access_host_array) $databases[$databases_row['dbserver']][] = $databases_row['databasename']; } + $customers_sel = Database::query(" + SELECT DISTINCT c.loginname + FROM `" . TABLE_PANEL_CUSTOMERS . "` c + LEFT JOIN `" . TABLE_PANEL_DATABASES . "` d ON c.customerid = d.customerid + WHERE c.deactivated = '0' AND d.id IS NOT NULL + "); + $customers = []; + while ($customer = $customers_sel->fetch(\PDO::FETCH_ASSOC)) { + $customers[] = $customer['loginname']; + } + $dbservers_stmt = Database::query("SELECT DISTINCT `dbserver` FROM `" . TABLE_PANEL_DATABASES . "`"); while ($dbserver = $dbservers_stmt->fetch(PDO::FETCH_ASSOC)) { + + // add all customer loginnames to the $databases array for this database-server to correct + // a possible existing global mysql-user for that customer + foreach ($customers as $customer) { + $databases[$dbserver['dbserver']][] = $customer; + } + // require privileged access for target db-server Database::needRoot(true, $dbserver['dbserver'], false); @@ -136,6 +154,8 @@ public static function correctMysqlUsers(array $mysql_access_host_array) $dbm->getManager()->flushPrivileges(); Database::needRoot(false); + + unset($databases[$dbserver['dbserver']]); } } @@ -149,13 +169,14 @@ public static function correctMysqlUsers(array $mysql_access_host_array) * @param ?string $password * @param int $dbserver * @param int $last_accnumber + * @param ?string $global_user * * @return string|bool $username if successful or false of username is equal to the password * @throws Exception */ - public function createDatabase(string $loginname = null, string $password = null, int $dbserver = 0, int $last_accnumber = 0) + public function createDatabase(string $loginname = null, string $password = null, int $dbserver = 0, int $last_accnumber = 0, string $global_user = "") { - Database::needRoot(true, $dbserver, false); + Database::needRoot(true, $dbserver, true); // check whether we shall create a random username if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'RANDOM') { @@ -184,6 +205,9 @@ public function createDatabase(string $loginname = null, string $password = null // and 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) { $this->getManager()->grantPrivilegesTo($username, $password, $mysql_access_host); + if (!empty($global_user)) { + $this->getManager()->grantCreateToDb($global_user, $username, $mysql_access_host); + } } $this->getManager()->flushPrivileges(); diff --git a/lib/Froxlor/Database/Manager/DbManagerMySQL.php b/lib/Froxlor/Database/Manager/DbManagerMySQL.php index 481c4dc323..178354cdfc 100644 --- a/lib/Froxlor/Database/Manager/DbManagerMySQL.php +++ b/lib/Froxlor/Database/Manager/DbManagerMySQL.php @@ -110,13 +110,21 @@ public function grantPrivilegesTo(string $username, $password, string $access_ho "password" => $password ]); // grant privileges + $grants = "ALL"; + if ($grant_access_prefix) { + $grants = "SELECT, INSERT, UPDATE, DELETE, DROP, INDEX, ALTER"; + } $stmt = Database::prepare(" - GRANT ALL ON `" . $username . ($grant_access_prefix ? '%' : '') . "`.* TO :username@:host + GRANT " . $grants . " ON `" . $username . ($grant_access_prefix ? '%' : '') . "`.* TO :username@:host "); Database::pexecute($stmt, [ "username" => $username, "host" => $access_host ]); + + if ($grant_access_prefix) { + $this->grantCreateToCustomerDbs($username, $access_host); + } } else { // set password if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.6', '<') || version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '10.0.0', '>=')) { @@ -145,9 +153,10 @@ public function grantPrivilegesTo(string $username, $password, string $access_ho * takes away any privileges from a user to that db * * @param string $dbname + * @param ?string $global_user * @throws \Exception */ - public function deleteDatabase(string $dbname) + public function deleteDatabase(string $dbname, string $global_user = "") { if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) { // failsafe if user has been deleted manually (requires MySQL 4.1.2+) @@ -167,11 +176,19 @@ public function deleteDatabase(string $dbname) } else { $drop_stmt = Database::prepare("DROP USER IF EXISTS :dbname@:host"); } + $rev_stmt = Database::prepare("REVOKE ALL PRIVILEGES ON `" . $dbname . "`.* FROM :guser@:host;"); while ($host = $host_res_stmt->fetch(PDO::FETCH_ASSOC)) { Database::pexecute($drop_stmt, [ 'dbname' => $dbname, 'host' => $host['Host'] ], false); + + if (!empty($global_user)) { + Database::pexecute($rev_stmt, [ + 'guser' => $global_user, + 'host' => $host['Host'] + ], false); + } } $drop_stmt = Database::prepare("DROP DATABASE IF EXISTS `" . $dbname . "`"); @@ -231,8 +248,15 @@ public function enableUser(string $username, string $host, bool $grant_access_pr { // 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 . '`'); + $grants = "ALL PRIVILEGES"; + if ($grant_access_prefix) { + $grants = "SELECT, INSERT, UPDATE, DELETE, DROP, INDEX, ALTER"; + } + Database::query('GRANT ' . $grants . ' ON `' . $username . ($grant_access_prefix ? '%' : '') . '`.* TO `' . $username . '`@`' . $host . '`'); + Database::query('GRANT ' . $grants . ' ON `' . str_replace('_', '\_', $username) . ($grant_access_prefix ? '%' : '') . '` . * TO `' . $username . '`@`' . $host . '`'); + if ($grant_access_prefix) { + $this->grantCreateToCustomerDbs($username, $host); + } } } @@ -292,4 +316,50 @@ public function getAllSqlUsers(bool $user_only = true): array } return $allsqlusers; } + + /** + * grant "CREATE" for prefix user to all existing databases of that customer + * + * @param string $username + * @param string $access_host + * @return void + * @throws \Exception + */ + private function grantCreateToCustomerDbs(string $username, string $access_host) + { + $cus_stmt = Database::prepare("SELECT customerid FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE loginname = :username"); + $cust = Database::pexecute_first($cus_stmt, ['username' => $username]); + if ($cust) { + $sel_stmt = Database::prepare("SELECT databasename FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid"); + Database::pexecute($sel_stmt, ['cid' => $cust['customerid']]); + while ($dbdata = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) { + $stmt = Database::prepare(" + GRANT CREATE ON `" . $dbdata['databasename'] . "`.* TO :username@:host + "); + Database::pexecute($stmt, [ + "username" => $username, + "host" => $access_host + ]); + } + } + } + + /** + * grant "CREATE" for prefix user to all existing databases of that customer + * + * @param string $username + * @param string $access_host + * @return void + * @throws \Exception + */ + public function grantCreateToDb(string $username, string $database, string $access_host) + { + $stmt = Database::prepare(" + GRANT CREATE ON `" . $database . "`.* TO :username@:host + "); + Database::pexecute($stmt, [ + "username" => $username, + "host" => $access_host + ]); + } } diff --git a/lib/Froxlor/Froxlor.php b/lib/Froxlor/Froxlor.php index dadd7cae46..a2e3a4657d 100644 --- a/lib/Froxlor/Froxlor.php +++ b/lib/Froxlor/Froxlor.php @@ -34,7 +34,7 @@ final class Froxlor const VERSION = '2.2.5'; // Database version (YYYYMMDDC where C is a daily counter) - const DBVERSION = '202411200'; + const DBVERSION = '202412030'; // Distribution branding-tag (used for Debian etc.) const BRANDING = '';