Skip to content

Commit

Permalink
Implement DSN-based database connection
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Alexey Abel committed May 5, 2024
1 parent d6c8a61 commit c130ea6
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 392 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Addded

* 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.
* `dsn` configuration key

### Removed

* 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.

## [1.5.1] - 2024-05-01

### Fixed
Expand Down
46 changes: 24 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ 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.

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

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

### 1. Database

that *User Backend SQL Raw* will connect to.

| key | value | default value |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------- |
| `db_type` | `postgresql` or `mariadb` | `postgresql` |
| `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` |
| `db_port` | your db port | `5432` |
| `db_name` | your db name | |
| `db_user` | your db user | |
| `db_password` | your db password | |
| `db_password_file` | path to file containing the db password | |
| `mariadb_charset` | the charset for mariadb connections | `utf8mb4` |

* Values without a default value are mandatory, except that
* only one of `db_password` or `db_passowrd_file` must be set.
* Only the first line of the file specified by `db_passowrd_file` is read.
that *User Backend SQL Raw* will connect to. There are two mutually exclusive ways to configure the database connection:
1. PostgreSQL-like
* Set `dsn` (containing user and password) and CAN specify `db_user` and (`db_password` or `db_password_file`). Values in DSN have priority.
2. MySQL-like
* Set `dsn` (not containing user and password) and MUST specify `db_user` and (`db_password` or `db_password_file`).

* `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:
* connect to PostgreSQL via a socket with ident authentication which requires no user or password at all: `pgsql:host=/var/run/postgresql;dbname=theNameOfYourUserDb`
* connect to PostgreSQL via TCP and user/password authentication: `pgsql:host=localhost;port=5432;dbname=theNameOfYourUserDb;user=theNameOfYourDbUser;password=thePasswordForTheDbUser`
* connect to MySQL via socket which requires no user or password at all: `mysql:unix_socket=/var/run/mysql/mysql.sock;dbname=theNameOfYourUserDb`
* 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`)
* `db_user`: Needs only be set for "MySQL-type" databases and is the database user that will be used to connect to the database.
* `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.
* `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.
* Only the first line of the file specified by `db_password_file` is read.
* Not more than 100 characters of the first line are read.
* Whitespace-like characters are [stripped](https://www.php.net/manual/en/function.trim.php) from
* Whitespace-like characters are [trimmed](https://www.php.net/manual/en/function.trim.php) from
the beginning and end of the read password.
* If you specify a socket as `db_host` (only for PostgreSQL), you need to put
dummy values for the mandatory values, although they are not required for the
socket connection. This will be fixed in a future release.

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.

### 2. SQL Queries

Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ In contrast to the app *SQL user backend*, you write the SQL queries yourself. Y
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.
This app supports PostgreSQL and MariaDB/MySQL.]]></description>
<version>1.5.1</version>
<version>2.0.0</version>
<licence>agpl</licence>
<author mail="[email protected]" >Alexey Abel</author>
<namespace>UserBackendSqlRaw</namespace>
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
}
},
"require": {
"php": ">=7.0"
"php": ">=8.0"
},
"require-dev": {
"php": ">=7.3",
"php": ">=8.0",
"phpunit/phpunit": "^9"
}
}
8 changes: 4 additions & 4 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

namespace OCA\UserBackendSqlRaw\AppInfo;

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

Expand Down
139 changes: 39 additions & 100 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,14 @@

class Config
{

const DEFAULT_DB_TYPE = 'postgresql';
const DEFAULT_DB_HOST = 'localhost';
const DEFAULT_POSTGRESQL_PORT = '5432';
const DEFAULT_MARIADB_PORT = '3306';
const DEFAULT_MARIADB_CHARSET = 'utf8mb4';
const DEFAULT_HASH_ALGORITHM_FOR_NEW_PASSWORDS = 'bcrypt';

const MAXIMUM_ALLOWED_PASSWORD_LENGTH = 100;

const CONFIG_KEY = 'user_backend_sql_raw';
const CONFIG_KEY_DB_TYPE = 'db_type';
const CONFIG_KEY_DB_HOST = 'db_host';
const CONFIG_KEY_DB_PORT = 'db_port';
const CONFIG_KEY_DB_NAME = 'db_name';
const CONFIG_KEY_DSN = 'dsn';
const CONFIG_KEY_DB_USER = 'db_user';
const CONFIG_KEY_DB_PASSWORD = 'db_password';
const CONFIG_KEY_DB_PASSWORD_FILE = 'db_password_file';
const CONFIG_KEY_MARIADB_CHARSET = 'mariadb_charset';
const CONFIG_KEY_HASH_ALGORITHM_FOR_NEW_PASSWORDS = 'hash_algorithm_for_new_passwords';

const CONFIG_KEY_QUERIES = 'queries';
Expand Down Expand Up @@ -81,64 +70,43 @@ public function __construct(LoggerInterface $logger, IConfig $nextCloudConfigura
. self::CONFIG_KEY . ' which should contain the configuration '
. 'for the app user_backend_sql_raw.');
}
}

/**
* @return string db type to connect to
*/
public function getDbType()
{
$dbTypeFromConfig = $this->getConfigValueOrDefaultValue(self::CONFIG_KEY_DB_TYPE
, self::DEFAULT_DB_TYPE);

$normalizedDbType = $this->normalize($dbTypeFromConfig);

if (!$this->dbTypeIsSupported($normalizedDbType)) {
throw new \UnexpectedValueException('The config key '
. self::CONFIG_KEY_DB_TYPE . ' is set to ' . $dbTypeFromConfig . '. This '
. 'value is invalid. Only postgresql and mariadb are supported.');
}

return $normalizedDbType;
}

/**
* @return string db host to connect to
*/
public function getDbHost()
{
return $this->getConfigValueOrDefaultValue(self::CONFIG_KEY_DB_HOST
, self::DEFAULT_DB_HOST);
$this->warnAboutObsoleteConfigKeys();
}

/**
* @return int db port to connect to
*/
public function getDbPort()
public function warnAboutObsoleteConfigKeys()
{

$defaultPortForCurrentDb = ($this->getDbType() === 'mariadb')
? self::DEFAULT_MARIADB_PORT
: self::DEFAULT_POSTGRESQL_PORT;

return $this->getConfigValueOrDefaultValue(self::CONFIG_KEY_DB_PORT
, $defaultPortForCurrentDb);
$obsolete_keys = array("db_type", "db_host", "db_port", "db_name", "mariadb_charset");
foreach ($obsolete_keys as $key) {
// not using getConfigValueOrFalse() here, because we want to also catch empty strings
if (array_key_exists(key: $key, array:$this->appConfiguration)) {
$this->logger->warning("The configuration key '{$key}' is obsolete since "
. "version 2.0.0. It has no effect and can be removed.");
}
}
}

/**
* @return string db name to connect to
* @return string dsn to use for db connection
* @throws \UnexpectedValueException
*/
public function getDbName()
public function getDsn()
{
return $this->getConfigValueOrThrowException(self::CONFIG_KEY_DB_NAME);
return $this->getConfigValueOrThrowException(self::CONFIG_KEY_DSN);
}

/**
* @return string db user to connect as
*/
public function getDbUser()
{
return $this->getConfigValueOrThrowException(self::CONFIG_KEY_DB_USER);
return $this->getConfigValueOrFalse(self::CONFIG_KEY_DB_USER);
}

// Used instead of getDbPassword() when only needs to check if `db_password`
// and not `db_password_file` or password in DSN is set.
public function dbPasswordInConfigIsSet() : bool {
return $this->getConfigValueOrFalse(self::CONFIG_KEY_DB_PASSWORD) !== false;
}

/**
Expand All @@ -147,24 +115,17 @@ public function getDbUser()
*/
public function getDbPassword()
{

$password = $this->getConfigValueOrFalse(self::CONFIG_KEY_DB_PASSWORD);
$passwordFilePath = $this->getConfigValueOrFalse(self::CONFIG_KEY_DB_PASSWORD_FILE);

$passwordIsSet = $password !== false;
$passwordFileIsSet = $passwordFilePath !== false;

if ($passwordIsSet === $passwordFileIsSet) { // expression is a "not XOR"
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.');
}

if ($passwordIsSet) {
$this->logger->debug("Will use db password specified directly in config.php.");
return $password;
}

// Password from file (db_password_file) has higher priority than password from config (db_password).
if ($passwordFileIsSet) {
$this->logger->debug("Will use db password stored in file " . $passwordFilePath) . ".";
$this->logger->debug("Will read db password stored in file " . $passwordFilePath)
. ". Password from config file will not be considered. Password from DSN still has "
."priority.";
$error_message_prefix = "Specified db password file with path {$passwordFilePath}";

if (!file_exists($passwordFilePath)) {
Expand All @@ -189,17 +150,19 @@ public function getDbPassword()
fclose($file);
$this->logger->debug("Successfully read db password from file " . $passwordFilePath) . ".";
return trim($first_line);
} elseif ($passwordIsSet) {
$this->logger->debug("Will read db password specified in config.php. Password from file"
." was not specified. Password from DSN still has priority.");
return $password;
} else {
return false;
}

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

/**
* @return string charset for mariadb connection
*/
public function getMariadbCharset()
{
return $this->getConfigValueOrDefaultValue(self::CONFIG_KEY_MARIADB_CHARSET
, self::DEFAULT_MARIADB_CHARSET);
}

/**
Expand All @@ -219,23 +182,6 @@ public function getHashAlgorithmForNewPasswords()
. 'to ' . $hashAlgorithmFromConfig . '. This value is invalid. Only '
. 'md5, sha256, sha512, bcrypt, argon2i and argon2id are supported.');
}

if ($normalizedHashAlgorithm === 'argon2i'
&& version_compare(PHP_VERSION, '7.2.0', '<')) {
throw new \UnexpectedValueException(
'You specified Argon2i as the hash algorithm for new '
. 'passwords. Argon2i is only available in PHP version 7.2.0 and'
. ' higher, but your PHP version is ' . PHP_VERSION . '.');
}

if ($normalizedHashAlgorithm === 'argon2id'
&& version_compare(PHP_VERSION, '7.3.0', '<')) {
throw new \UnexpectedValueException(
'You specified Argon2id as the hash algorithm for new '
. 'passwords. Argon2id is only available in PHP version 7.3.0 and'
. ' higher, but your PHP version is ' . PHP_VERSION . '.');
}

return $normalizedHashAlgorithm;
}

Expand Down Expand Up @@ -289,6 +235,8 @@ public function getQueryCreateUser()
return $this->getQueryStringOrFalse(self::CONFIG_KEY_CREATE_USER);
}



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

/**
* @param $dbType string db descriptor to check
* @return bool whether the db is supported
*/
private function dbTypeIsSupported($dbType)
{
return $dbType === 'postgresql'
|| $dbType === 'mariadb';
}

/**
* Checks whether hash algorithm is supported for writing.
* @param $hashAlgorithm string hash algorithm descriptor to check
Expand All @@ -400,4 +338,5 @@ private function normalize($string)
{
return strtolower(preg_replace("/[-_]/", "", $string));
}

}
Loading

0 comments on commit c130ea6

Please sign in to comment.