fix permissions of global mysql-user for customers; fixes #1286

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann
2024-12-03 17:01:28 +01:00
parent 2bb863baac
commit 079047b9fe
8 changed files with 142 additions and 13 deletions

View File

@@ -228,7 +228,7 @@ if ($page == 'overview' || $page == 'mysqls') {
$new_password = Crypt::validatePassword(Request::post('mysql_password')); $new_password = Crypt::validatePassword(Request::post('mysql_password'));
foreach ($allowed_mysqlservers as $dbserver) { foreach ($allowed_mysqlservers as $dbserver) {
// require privileged access for target db-server // require privileged access for target db-server
Database::needRoot(true, $dbserver, false); Database::needRoot(true, $dbserver, true);
// get DbManager // get DbManager
$dbm = new DbManager($log); $dbm = new DbManager($log);
// give permission to the user on every access-host we have // give permission to the user on every access-host we have

View File

@@ -734,7 +734,7 @@ opcache.validate_timestamps'),
('panel', 'settings_mode', '0'), ('panel', 'settings_mode', '0'),
('panel', 'menu_collapsed', '1'), ('panel', 'menu_collapsed', '1'),
('panel', 'version', '2.2.5'), ('panel', 'version', '2.2.5'),
('panel', 'db_version', '202411200'); ('panel', 'db_version', '202412030');
DROP TABLE IF EXISTS `panel_tasks`; DROP TABLE IF EXISTS `panel_tasks`;

View File

@@ -24,6 +24,7 @@
*/ */
use Froxlor\Database\Database; use Froxlor\Database\Database;
use Froxlor\Database\DbManager;
use Froxlor\Froxlor; use Froxlor\Froxlor;
use Froxlor\Install\Update; use Froxlor\Install\Update;
use Froxlor\Settings; use Froxlor\Settings;
@@ -209,3 +210,37 @@ if (Froxlor::isDatabaseVersion('202409280')) {
Froxlor::updateToDbVersion('202411200'); 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');
}

View File

@@ -1347,7 +1347,7 @@ class Customers extends ApiCommand implements ResourceEntity
$current_allowed_mysqlserver = isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_mysqlserver'], true) : []; $current_allowed_mysqlserver = isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_mysqlserver'], true) : [];
foreach ($current_allowed_mysqlserver as $dbserver) { foreach ($current_allowed_mysqlserver as $dbserver) {
// require privileged access for target db-server // require privileged access for target db-server
Database::needRoot(true, $dbserver, false); Database::needRoot(true, $dbserver, true);
// get DbManager // get DbManager
$dbm = new DbManager($this->logger()); $dbm = new DbManager($this->logger());
foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {

View File

@@ -113,9 +113,9 @@ class Mysqls extends ApiCommand implements ResourceEntity
if (strlen($newdb_params['loginname'] . '_' . $databasename) > Database::getSqlUsernameLength()) { 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); 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 { } 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 // we've checked against the password in dbm->createDatabase
@@ -541,7 +541,7 @@ class Mysqls extends ApiCommand implements ResourceEntity
// Begin root-session // Begin root-session
Database::needRoot(true, $result['dbserver'], false); Database::needRoot(true, $result['dbserver'], false);
$dbm = new DbManager($this->logger()); $dbm = new DbManager($this->logger());
$dbm->getManager()->deleteDatabase($result['databasename']); $dbm->getManager()->deleteDatabase($result['databasename'], $customer['loginname']);
Database::needRoot(false); Database::needRoot(false);
// End root-session // End root-session

View File

@@ -102,8 +102,26 @@ class DbManager
$databases[$databases_row['dbserver']][] = $databases_row['databasename']; $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 . "`"); $dbservers_stmt = Database::query("SELECT DISTINCT `dbserver` FROM `" . TABLE_PANEL_DATABASES . "`");
while ($dbserver = $dbservers_stmt->fetch(PDO::FETCH_ASSOC)) { 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 // require privileged access for target db-server
Database::needRoot(true, $dbserver['dbserver'], false); Database::needRoot(true, $dbserver['dbserver'], false);
@@ -136,6 +154,8 @@ class DbManager
$dbm->getManager()->flushPrivileges(); $dbm->getManager()->flushPrivileges();
Database::needRoot(false); Database::needRoot(false);
unset($databases[$dbserver['dbserver']]);
} }
} }
@@ -149,13 +169,14 @@ class DbManager
* @param ?string $password * @param ?string $password
* @param int $dbserver * @param int $dbserver
* @param int $last_accnumber * @param int $last_accnumber
* @param ?string $global_user
* *
* @return string|bool $username if successful or false of username is equal to the password * @return string|bool $username if successful or false of username is equal to the password
* @throws Exception * @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 // check whether we shall create a random username
if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'RANDOM') { if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'RANDOM') {
@@ -184,6 +205,9 @@ class DbManager
// and give permission to the user on every access-host we have // 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) { foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) {
$this->getManager()->grantPrivilegesTo($username, $password, $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(); $this->getManager()->flushPrivileges();

View File

@@ -110,13 +110,21 @@ class DbManagerMySQL
"password" => $password "password" => $password
]); ]);
// grant privileges // grant privileges
$grants = "ALL";
if ($grant_access_prefix) {
$grants = "SELECT, INSERT, UPDATE, DELETE, DROP, INDEX, ALTER";
}
$stmt = Database::prepare(" $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, [ Database::pexecute($stmt, [
"username" => $username, "username" => $username,
"host" => $access_host "host" => $access_host
]); ]);
if ($grant_access_prefix) {
$this->grantCreateToCustomerDbs($username, $access_host);
}
} else { } else {
// set password // 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', '>=')) { 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 @@ class DbManagerMySQL
* takes away any privileges from a user to that db * takes away any privileges from a user to that db
* *
* @param string $dbname * @param string $dbname
* @param ?string $global_user
* @throws \Exception * @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', '<')) { if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) {
// failsafe if user has been deleted manually (requires MySQL 4.1.2+) // failsafe if user has been deleted manually (requires MySQL 4.1.2+)
@@ -167,11 +176,19 @@ class DbManagerMySQL
} else { } else {
$drop_stmt = Database::prepare("DROP USER IF EXISTS :dbname@:host"); $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)) { while ($host = $host_res_stmt->fetch(PDO::FETCH_ASSOC)) {
Database::pexecute($drop_stmt, [ Database::pexecute($drop_stmt, [
'dbname' => $dbname, 'dbname' => $dbname,
'host' => $host['Host'] 'host' => $host['Host']
], false); ], 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 . "`"); $drop_stmt = Database::prepare("DROP DATABASE IF EXISTS `" . $dbname . "`");
@@ -231,8 +248,15 @@ class DbManagerMySQL
{ {
// check whether user exists to avoid errors // check whether user exists to avoid errors
if ($this->userExistsOnHost($username, $host)) { if ($this->userExistsOnHost($username, $host)) {
Database::query('GRANT ALL PRIVILEGES ON `' . $username . ($grant_access_prefix ? '%' : '') . '`.* TO `' . $username . '`@`' . $host . '`'); $grants = "ALL PRIVILEGES";
Database::query('GRANT ALL PRIVILEGES ON `' . str_replace('_', '\_', $username) . ($grant_access_prefix ? '%' : '') . '` . * TO `' . $username . '`@`' . $host . '`'); 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 @@ class DbManagerMySQL
} }
return $allsqlusers; 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
]);
}
} }

View File

@@ -34,7 +34,7 @@ final class Froxlor
const VERSION = '2.2.5'; const VERSION = '2.2.5';
// Database version (YYYYMMDDC where C is a daily counter) // Database version (YYYYMMDDC where C is a daily counter)
const DBVERSION = '202411200'; const DBVERSION = '202412030';
// Distribution branding-tag (used for Debian etc.) // Distribution branding-tag (used for Debian etc.)
const BRANDING = ''; const BRANDING = '';