Skip to content

Commit c130ea6

Browse files
author
Alexey Abel
committed
Implement DSN-based database connection
The DSN-based connection configuration is closer to what PHP does with PDO objects under the hood and this allows the use of more scenarios such as socket connections and databases out-of-the-box without user_backend_sql_raw having to provide configuration parameters for each option.
1 parent d6c8a61 commit c130ea6

File tree

11 files changed

+159
-392
lines changed

11 files changed

+159
-392
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Addded
11+
12+
* DSN-based database connection mechanism. This enables support for socket-based database connections and also connections to Firebird, MS SQL, Oracle DB, ODBC, DB2, SQLite, Informix and IBM databases - basically whatever the [PHP PDO-driver](https://www.php.net/manual/en/pdo.drivers.php) supports. But PostgreSQL remains the only tested database and MySQL/MariaDB to some degree. The other databaes should "just work", but this has not been tested.
13+
* `dsn` configuration key
14+
15+
### Removed
16+
17+
* configuration keys `db_type`, `db_host`, `db_port`, `db_name`, `mariadb_charset`. These settings must now be included in the DSN string. See [README.md](README.md) on how to do this.
18+
819
## [1.5.1] - 2024-05-01
920

1021
### Fixed

README.md

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ Argon2id. Because the various formats are recognized on-the-fly your db can can
2222
have differing hash string formats at the same time, which eases migration to
2323
newer formats.
2424

25-
This app supports PostgreSQL and MariaDB/MySQL.
25+
This app primarily supports PostgreSQL and MariaDB/MySQL but the underlying PHP
26+
[mechanism](https://www.php.net/manual/en/pdo.drivers.php) also supports
27+
Firebird, MS SQL, Oracle DB, ODBC, DB2, SQLite, Informix and IBM databases. By
28+
using an appropriate DSN you should be able to connect to these databases. This
29+
has not been tested, though.
2630

2731
See [CHANGELOG.md](CHANGELOG.md) for changes in newer versions. This app follows
2832
semantic versioning and there should not be any breaking changes unless the
@@ -45,7 +49,7 @@ This app has no user interface. All configuration is done via Nextcloud's system
4549
//'db_type' => 'postgresql',
4650
//'db_host' => 'localhost',
4751
//'db_port' => '5432',
48-
'db_name' => 'theNameOfYourUserDatabase',
52+
'db_name' => 'theNameOfYourDbUser',
4953
'db_user' => 'yourDatabaseUser',
5054
'db_password' => 'thePasswordForTheDatabaseUser',
5155
//'db_password_file' => '/var/secrets/fileContainingThePasswordForTheDatabaseUser',
@@ -70,28 +74,26 @@ There are three types of configuration parameters:
7074

7175
### 1. Database
7276

73-
that *User Backend SQL Raw* will connect to.
74-
75-
| key | value | default value |
76-
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------- |
77-
| `db_type` | `postgresql` or `mariadb` | `postgresql` |
78-
| `db_host` | your db host such as `localhost` or `db.example.com` or (only for PostgreSQL) path to socket, e.g. `/var/run/postgresql` | `localhost` |
79-
| `db_port` | your db port | `5432` |
80-
| `db_name` | your db name | |
81-
| `db_user` | your db user | |
82-
| `db_password` | your db password | |
83-
| `db_password_file` | path to file containing the db password | |
84-
| `mariadb_charset` | the charset for mariadb connections | `utf8mb4` |
85-
86-
* Values without a default value are mandatory, except that
87-
* only one of `db_password` or `db_passowrd_file` must be set.
88-
* Only the first line of the file specified by `db_passowrd_file` is read.
77+
that *User Backend SQL Raw* will connect to. There are two mutually exclusive ways to configure the database connection:
78+
1. PostgreSQL-like
79+
* Set `dsn` (containing user and password) and CAN specify `db_user` and (`db_password` or `db_password_file`). Values in DSN have priority.
80+
2. MySQL-like
81+
* Set `dsn` (not containing user and password) and MUST specify `db_user` and (`db_password` or `db_password_file`).
82+
83+
* `dsn`: check how to construct DSNs for [PostgreSQL](https://www.php.net/manual/en/ref.pdo-pgsql.connection.php) and [MySQL](https://www.php.net/manual/en/ref.pdo-mysql.connection.php) Examples:
84+
* connect to PostgreSQL via a socket with ident authentication which requires no user or password at all: `pgsql:host=/var/run/postgresql;dbname=theNameOfYourUserDb`
85+
* connect to PostgreSQL via TCP and user/password authentication: `pgsql:host=localhost;port=5432;dbname=theNameOfYourUserDb;user=theNameOfYourDbUser;password=thePasswordForTheDbUser`
86+
* connect to MySQL via socket which requires no user or password at all: `mysql:unix_socket=/var/run/mysql/mysql.sock;dbname=theNameOfYourUserDb`
87+
* connect to MySQL via TCP and user/password authentication: `mysql:host=localhost;port=3306;dbname=testdb` and then also set `db_user` and (`db_password` or `db_password_file`)
88+
* `db_user`: Needs only be set for "MySQL-type" databases and is the database user that will be used to connect to the database.
89+
* `db_password`: Needs only be set for "MySQL-type" databases and is the password for the user that will be used to connect to the database.
90+
* `db_password_file`: Can be set to read the password from a file. Has higher priority than `db_password`, but lower priority than password in DSN. So, for PostgreSQL-like database connections, don't specify the password in the DSN because it would override this. For MySQL-like connections, this one will have priority.
91+
* Only the first line of the file specified by `db_password_file` is read.
8992
* Not more than 100 characters of the first line are read.
90-
* Whitespace-like characters are [stripped](https://www.php.net/manual/en/function.trim.php) from
93+
* Whitespace-like characters are [trimmed](https://www.php.net/manual/en/function.trim.php) from
9194
the beginning and end of the read password.
92-
* If you specify a socket as `db_host` (only for PostgreSQL), you need to put
93-
dummy values for the mandatory values, although they are not required for the
94-
socket connection. This will be fixed in a future release.
95+
96+
For other databases check their [PDO driver documentation pages](https://www.php.net/manual/en/pdo.drivers.php) which in-turn link to their respective DSN references.
9597

9698
### 2. SQL Queries
9799

appinfo/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ In contrast to the app *SQL user backend*, you write the SQL queries yourself. Y
1414
The app uses prepared statements and is written to be secure by default to prevent SQL injections. It understands the most popular standards for password hash formats: MD5-CRYPT, SHA256-CRYPT, SHA512-CRYPT, BCrypt and the state-of-the-art Argon2i and Argon2id. Because the various formats are recognized on-the-fly your db can can have differing hash string formats at the same time, which eases migration to newer formats.
1515
1616
This app supports PostgreSQL and MariaDB/MySQL.]]></description>
17-
<version>1.5.1</version>
17+
<version>2.0.0</version>
1818
<licence>agpl</licence>
1919
<author mail="dev@abelonline.de" >Alexey Abel</author>
2020
<namespace>UserBackendSqlRaw</namespace>

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
}
2525
},
2626
"require": {
27-
"php": ">=7.0"
27+
"php": ">=8.0"
2828
},
2929
"require-dev": {
30-
"php": ">=7.3",
30+
"php": ">=8.0",
3131
"phpunit/phpunit": "^9"
3232
}
3333
}

lib/AppInfo/Application.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
namespace OCA\UserBackendSqlRaw\AppInfo;
2323

24+
use OCA\UserBackendSqlRaw\Db;
2425
use OCP\AppFramework\Bootstrap\IBootContext;
2526
use OCP\AppFramework\Bootstrap\IBootstrap;
2627
use OCP\AppFramework\Bootstrap\IRegistrationContext;
@@ -49,10 +50,9 @@ public function register(IRegistrationContext $context): void
4950
* Nextcloud's dependency injection is partly explained in:
5051
* https://docs.nextcloud.com/server/latest/developer_manual/basics/dependency_injection.html#how-to-deal-with-interface-and-primitive-type-parameters
5152
*/
52-
$context->registerService('OCA\UserBackendSqlRaw\Db', function (ContainerInterface $container) {
53-
/** @var \OCA\UserBackendSqlRaw\Config $config */
54-
$config = $container->get('OCA\UserBackendSqlRaw\Config');
55-
return $container->get('OCA\UserBackendSqlRaw\Dbs\\' . ucfirst($config->getDbType()));
53+
$context->registerService(OCA\UserBackendSqlRaw\Db::class, function (ContainerInterface $container) {
54+
// TODO: can be simplified/removed probably
55+
return new OCA\UserBackendSqlRaw\Db();
5656
});
5757
}
5858

lib/Config.php

Lines changed: 39 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,14 @@
2626

2727
class Config
2828
{
29-
30-
const DEFAULT_DB_TYPE = 'postgresql';
31-
const DEFAULT_DB_HOST = 'localhost';
32-
const DEFAULT_POSTGRESQL_PORT = '5432';
33-
const DEFAULT_MARIADB_PORT = '3306';
34-
const DEFAULT_MARIADB_CHARSET = 'utf8mb4';
3529
const DEFAULT_HASH_ALGORITHM_FOR_NEW_PASSWORDS = 'bcrypt';
36-
3730
const MAXIMUM_ALLOWED_PASSWORD_LENGTH = 100;
3831

3932
const CONFIG_KEY = 'user_backend_sql_raw';
40-
const CONFIG_KEY_DB_TYPE = 'db_type';
41-
const CONFIG_KEY_DB_HOST = 'db_host';
42-
const CONFIG_KEY_DB_PORT = 'db_port';
43-
const CONFIG_KEY_DB_NAME = 'db_name';
33+
const CONFIG_KEY_DSN = 'dsn';
4434
const CONFIG_KEY_DB_USER = 'db_user';
4535
const CONFIG_KEY_DB_PASSWORD = 'db_password';
4636
const CONFIG_KEY_DB_PASSWORD_FILE = 'db_password_file';
47-
const CONFIG_KEY_MARIADB_CHARSET = 'mariadb_charset';
4837
const CONFIG_KEY_HASH_ALGORITHM_FOR_NEW_PASSWORDS = 'hash_algorithm_for_new_passwords';
4938

5039
const CONFIG_KEY_QUERIES = 'queries';
@@ -81,64 +70,43 @@ public function __construct(LoggerInterface $logger, IConfig $nextCloudConfigura
8170
. self::CONFIG_KEY . ' which should contain the configuration '
8271
. 'for the app user_backend_sql_raw.');
8372
}
84-
}
85-
86-
/**
87-
* @return string db type to connect to
88-
*/
89-
public function getDbType()
90-
{
91-
$dbTypeFromConfig = $this->getConfigValueOrDefaultValue(self::CONFIG_KEY_DB_TYPE
92-
, self::DEFAULT_DB_TYPE);
93-
94-
$normalizedDbType = $this->normalize($dbTypeFromConfig);
95-
96-
if (!$this->dbTypeIsSupported($normalizedDbType)) {
97-
throw new \UnexpectedValueException('The config key '
98-
. self::CONFIG_KEY_DB_TYPE . ' is set to ' . $dbTypeFromConfig . '. This '
99-
. 'value is invalid. Only postgresql and mariadb are supported.');
100-
}
101-
102-
return $normalizedDbType;
103-
}
10473

105-
/**
106-
* @return string db host to connect to
107-
*/
108-
public function getDbHost()
109-
{
110-
return $this->getConfigValueOrDefaultValue(self::CONFIG_KEY_DB_HOST
111-
, self::DEFAULT_DB_HOST);
74+
$this->warnAboutObsoleteConfigKeys();
11275
}
11376

114-
/**
115-
* @return int db port to connect to
116-
*/
117-
public function getDbPort()
77+
public function warnAboutObsoleteConfigKeys()
11878
{
119-
120-
$defaultPortForCurrentDb = ($this->getDbType() === 'mariadb')
121-
? self::DEFAULT_MARIADB_PORT
122-
: self::DEFAULT_POSTGRESQL_PORT;
123-
124-
return $this->getConfigValueOrDefaultValue(self::CONFIG_KEY_DB_PORT
125-
, $defaultPortForCurrentDb);
79+
$obsolete_keys = array("db_type", "db_host", "db_port", "db_name", "mariadb_charset");
80+
foreach ($obsolete_keys as $key) {
81+
// not using getConfigValueOrFalse() here, because we want to also catch empty strings
82+
if (array_key_exists(key: $key, array:$this->appConfiguration)) {
83+
$this->logger->warning("The configuration key '{$key}' is obsolete since "
84+
. "version 2.0.0. It has no effect and can be removed.");
85+
}
86+
}
12687
}
12788

12889
/**
129-
* @return string db name to connect to
90+
* @return string dsn to use for db connection
91+
* @throws \UnexpectedValueException
13092
*/
131-
public function getDbName()
93+
public function getDsn()
13294
{
133-
return $this->getConfigValueOrThrowException(self::CONFIG_KEY_DB_NAME);
95+
return $this->getConfigValueOrThrowException(self::CONFIG_KEY_DSN);
13496
}
13597

13698
/**
13799
* @return string db user to connect as
138100
*/
139101
public function getDbUser()
140102
{
141-
return $this->getConfigValueOrThrowException(self::CONFIG_KEY_DB_USER);
103+
return $this->getConfigValueOrFalse(self::CONFIG_KEY_DB_USER);
104+
}
105+
106+
// Used instead of getDbPassword() when only needs to check if `db_password`
107+
// and not `db_password_file` or password in DSN is set.
108+
public function dbPasswordInConfigIsSet() : bool {
109+
return $this->getConfigValueOrFalse(self::CONFIG_KEY_DB_PASSWORD) !== false;
142110
}
143111

144112
/**
@@ -147,24 +115,17 @@ public function getDbUser()
147115
*/
148116
public function getDbPassword()
149117
{
150-
151118
$password = $this->getConfigValueOrFalse(self::CONFIG_KEY_DB_PASSWORD);
152119
$passwordFilePath = $this->getConfigValueOrFalse(self::CONFIG_KEY_DB_PASSWORD_FILE);
153120

154121
$passwordIsSet = $password !== false;
155122
$passwordFileIsSet = $passwordFilePath !== false;
156123

157-
if ($passwordIsSet === $passwordFileIsSet) { // expression is a "not XOR"
158-
throw new \UnexpectedValueException('Exactly one of ' . self::CONFIG_KEY_DB_PASSWORD . ' or ' . self::CONFIG_KEY_DB_PASSWORD_FILE . ' must be set (not be empty) in the config.');
159-
}
160-
161-
if ($passwordIsSet) {
162-
$this->logger->debug("Will use db password specified directly in config.php.");
163-
return $password;
164-
}
165-
124+
// Password from file (db_password_file) has higher priority than password from config (db_password).
166125
if ($passwordFileIsSet) {
167-
$this->logger->debug("Will use db password stored in file " . $passwordFilePath) . ".";
126+
$this->logger->debug("Will read db password stored in file " . $passwordFilePath)
127+
. ". Password from config file will not be considered. Password from DSN still has "
128+
."priority.";
168129
$error_message_prefix = "Specified db password file with path {$passwordFilePath}";
169130

170131
if (!file_exists($passwordFilePath)) {
@@ -189,17 +150,19 @@ public function getDbPassword()
189150
fclose($file);
190151
$this->logger->debug("Successfully read db password from file " . $passwordFilePath) . ".";
191152
return trim($first_line);
153+
} elseif ($passwordIsSet) {
154+
$this->logger->debug("Will read db password specified in config.php. Password from file"
155+
." was not specified. Password from DSN still has priority.");
156+
return $password;
157+
} else {
158+
return false;
192159
}
193160

194-
}
161+
// Priority of password in the DSN over both passwords read here is
162+
// implemented in the PDO implementation of PHP. It will simply ignore
163+
// the password given as a parameter during PDO object creation and use
164+
// the one from the DSN, if the DSN contains it.
195165

196-
/**
197-
* @return string charset for mariadb connection
198-
*/
199-
public function getMariadbCharset()
200-
{
201-
return $this->getConfigValueOrDefaultValue(self::CONFIG_KEY_MARIADB_CHARSET
202-
, self::DEFAULT_MARIADB_CHARSET);
203166
}
204167

205168
/**
@@ -219,23 +182,6 @@ public function getHashAlgorithmForNewPasswords()
219182
. 'to ' . $hashAlgorithmFromConfig . '. This value is invalid. Only '
220183
. 'md5, sha256, sha512, bcrypt, argon2i and argon2id are supported.');
221184
}
222-
223-
if ($normalizedHashAlgorithm === 'argon2i'
224-
&& version_compare(PHP_VERSION, '7.2.0', '<')) {
225-
throw new \UnexpectedValueException(
226-
'You specified Argon2i as the hash algorithm for new '
227-
. 'passwords. Argon2i is only available in PHP version 7.2.0 and'
228-
. ' higher, but your PHP version is ' . PHP_VERSION . '.');
229-
}
230-
231-
if ($normalizedHashAlgorithm === 'argon2id'
232-
&& version_compare(PHP_VERSION, '7.3.0', '<')) {
233-
throw new \UnexpectedValueException(
234-
'You specified Argon2id as the hash algorithm for new '
235-
. 'passwords. Argon2id is only available in PHP version 7.3.0 and'
236-
. ' higher, but your PHP version is ' . PHP_VERSION . '.');
237-
}
238-
239185
return $normalizedHashAlgorithm;
240186
}
241187

@@ -289,6 +235,8 @@ public function getQueryCreateUser()
289235
return $this->getQueryStringOrFalse(self::CONFIG_KEY_CREATE_USER);
290236
}
291237

238+
239+
292240
/**
293241
* Tries to read a config value and throws an exception if it is not set.
294242
* This is used for config keys that are mandatory.
@@ -364,16 +312,6 @@ private function getQueryStringOrFalse($configKey)
364312
return $this->getValueOrFalse($queryArray[$configKey] ?? false);
365313
}
366314

367-
/**
368-
* @param $dbType string db descriptor to check
369-
* @return bool whether the db is supported
370-
*/
371-
private function dbTypeIsSupported($dbType)
372-
{
373-
return $dbType === 'postgresql'
374-
|| $dbType === 'mariadb';
375-
}
376-
377315
/**
378316
* Checks whether hash algorithm is supported for writing.
379317
* @param $hashAlgorithm string hash algorithm descriptor to check
@@ -400,4 +338,5 @@ private function normalize($string)
400338
{
401339
return strtolower(preg_replace("/[-_]/", "", $string));
402340
}
341+
403342
}

0 commit comments

Comments
 (0)