From 8e2e1325a1fcd3b3ad8abd7d6f84310e002c2ffa Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 20 Mar 2026 08:05:59 -0400 Subject: [PATCH 1/2] refactor(setup/mysql): extract/add canCreateUsers helper Signed-off-by: Josh --- lib/private/Setup/MySQL.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/private/Setup/MySQL.php b/lib/private/Setup/MySQL.php index e656fe4b3a3fc..480ebaf90b60a 100644 --- a/lib/private/Setup/MySQL.php +++ b/lib/private/Setup/MySQL.php @@ -32,7 +32,7 @@ public function setupDatabase(): void { $connection = $this->connect(['dbname' => null]); } - if ($this->tryCreateDbUser) { + if ($this->tryCreateDbUser && $this->canCreateUsers($connection)) { $this->createSpecificUser('oc_admin', new ConnectionAdapter($connection)); } @@ -74,6 +74,24 @@ private function userExists(IDBConnection $connection, string $username): bool { return $exists; } + /** + * Check whether the current connection user has rights to create other users. + * + * In MySQL, the ability to SELECT from mysql.user is a sufficient proxy + * for administrative privileges — unprivileged users cannot read it. + * The actual createDBUser() call will fail with a clear error if the + * privilege assumption is wrong. + */ + private function canCreateUsers(IDBConnection $connection): bool { + try { + $connection->executeQuery('SELECT COUNT(*) FROM mysql.user LIMIT 1'); + return true; + } catch (\Exception $e) { + return false; + } + } + + /** * Find a username starting from $base that doesn't already exist, * respecting MySQL's 16-character username limit. From 25f9046b63c3d3bdae82add4f3ce8a93b51667e7 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 20 Mar 2026 08:07:07 -0400 Subject: [PATCH 2/2] refactor(setup/postgresql): extract canCreateUsers helper Signed-off-by: Josh --- lib/private/Setup/PostgreSQL.php | 72 +++++++++++++++++++------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/lib/private/Setup/PostgreSQL.php b/lib/private/Setup/PostgreSQL.php index e781ab3424853..daf6c821aec93 100644 --- a/lib/private/Setup/PostgreSQL.php +++ b/lib/private/Setup/PostgreSQL.php @@ -27,22 +27,7 @@ public function setupDatabase(): void { ]); if ($this->tryCreateDbUser) { //check for roles creation rights in postgresql - $builder = $connection->getQueryBuilder(); - $builder->automaticTablePrefix(false); - $query = $builder - ->select('rolname') - ->from('pg_roles') - ->where($builder->expr()->eq('rolcreaterole', new Literal('TRUE'))) - ->andWhere($builder->expr()->eq('rolname', $builder->createNamedParameter($this->dbUser))); - - try { - $result = $query->executeQuery(); - $canCreateRoles = $result->rowCount() > 0; - } catch (DatabaseException $e) { - $canCreateRoles = false; - } - - if ($canCreateRoles) { + if ($this->canCreateUsers($connection)) { $connectionMainDatabase = $this->connect(); //use the admin login data for the new database user @@ -52,21 +37,12 @@ public function setupDatabase(): void { $this->dbPassword = Server::get(ISecureRandom::class)->generate(30, ISecureRandom::CHAR_ALPHANUMERIC); $this->createDBUser($connection); - } - } - $this->config->setValues([ - 'dbuser' => $this->dbUser, - 'dbpassword' => $this->dbPassword, - ]); - - //create the database - $this->createDatabase($connection); - // the connection to dbname=postgres is not needed anymore - $connection->close(); + //create the database + $this->createDatabase($connection); + // the connection to dbname=postgres is not needed anymore + $connection->close(); - if ($this->tryCreateDbUser) { - if ($canCreateRoles) { // Go to the main database and grant create on the public schema // The code below is implemented to make installing possible with PostgreSQL version 15: // https://www.postgresql.org/docs/release/15.0/ @@ -76,10 +52,26 @@ public function setupDatabase(): void { // Also see https://www.postgresql.org/docs/15/ddl-schemas.html#DDL-SCHEMAS-PATTERNS $connectionMainDatabase->executeQuery('GRANT CREATE ON SCHEMA public TO "' . addslashes($this->dbUser) . '"'); $connectionMainDatabase->close(); + } else { + //create the database + $this->createDatabase($connection); + // the connection to dbname=postgres is not needed anymore + $connection->close(); } + } else { + //create the database + $this->createDatabase($connection); + // the connection to dbname=postgres is not needed anymore + $connection->close(); } + + $this->config->setValues([ + 'dbuser' => $this->dbUser, + 'dbpassword' => $this->dbPassword, + ]); } catch (\Exception $e) { - $this->logger->warning('Error trying to connect as "postgres", assuming database is setup and tables need to be created', [ + $this->logger->warning( + 'Error trying to connect as "postgres", assuming database is setup and tables need to be created', [ 'exception' => $e, ]); $this->config->setValues([ @@ -154,6 +146,26 @@ private function userExists(Connection $connection, string $username): bool { return $result->rowCount() > 0; } + /** + * Check whether the current connection user has the CREATEROLE privilege. + */ + private function canCreateUsers(Connection $connection): bool { + $builder = $connection->getQueryBuilder(); + $builder->automaticTablePrefix(false); + $query = $builder + ->select('rolname') + ->from('pg_roles') + ->where($builder->expr()->eq('rolcreaterole', new Literal('TRUE'))) + ->andWhere($builder->expr()->eq('rolname', $builder->createNamedParameter($this->dbUser))); + + try { + $result = $query->executeQuery(); + return $result->rowCount() > 0; + } catch (DatabaseException $e) { + return false; + } + } + private function databaseExists(Connection $connection): bool { $builder = $connection->getQueryBuilder(); $builder->automaticTablePrefix(false);