mirror of
https://github.com/archtechx/tenancy.git
synced 2026-08-06 05:54:03 +00:00
### Parameter validation In `statement()` calls of `TenantDatabaseManager`s, use parameter binding when possible. When that's not possible, validate the parameters using `validateParameter()` or `validatePassword()`. Passwords use a less strict allowlist than other parameters (e.g. DB names), since passwords tend to use more special characters but we can afford to be more restrictive in the generic `validateParameter()`. In `SQLiteDatabaseManager`, names of file-based databases are validated (in `createDatabase`, `deleteDatabase` and `databaseExists`) using a similar allowlist to the (non-password) parameters in other DB managers, with an additional character: `.` (this addition is necessary since file-based SQLite databases end with `.sqlite`). ### DatabaseTenancyBootstrapper - harden() and the lost test file While checking for more places that could use validation, I realized that it's possible to update tenant's db_name to the central DB or the DB of another tenant. Added the `DatabaseTenancyBootstrapper::$harden` property -- setting it to true prevents tenants from connecting to the wrong databases (`RuntimeException` is thrown after connecting to the wrong database). Also, the DatabaseTenancyBootstrapper test file was ignored while running tests because it lacked the `Test` suffix. Added the suffix and fixed the broken `DATABASE_URL` test in the file. ### SQLiteDatabaseManager - respect static $path property in makeConnectionConfig() SQLiteDatabaseManager had a bug in `makeConnectionConfig`: the method didn't respect the static `$path` property, it used `database_path()` instead. Added a regression test for that. Also recognizing in-memory SQLite databases (using `isInMemory()`) is more strict now so that simply having a db_name with `_tenancy_inmemory_` somewhere in the name doesn't make a file-based database considered in-memory. ### MySQLDatabaseManager - charset and collation defaulting Creating databases with `null` charsets and collations resulted in a `QueryException`, since null isn't a valid charset/collation. To solve that, in the `CREATE DATABASE` statement in MySQLDatabaseManager, only add charset/collation to the statement if they are not null. MySQL defaults to the server's charset and collation, so it's safe to not pass any charset/collation in the `CREATE DATABASE` statement and let MySQL choose. Also, if e.g. collation is non-null and charset is null, MySQL will use a charset compatible with the used collation, and this works both ways. --------- Co-authored-by: Samuel Stancl <samuel@archte.ch> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
96 lines
3.4 KiB
PHP
96 lines
3.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Stancl\Tenancy\Database\Concerns;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* Provides methods to validate database parameters (e.g. database names, usernames, passwords)
|
|
* before using them in SQL statements (or in file paths in the case of SQLiteDatabaseManager).
|
|
*
|
|
* Used where parameters can be provided by users, and where parameter binding cannot be used.
|
|
*
|
|
* @see \Stancl\Tenancy\Database\TenantDatabaseManagers\TenantDatabaseManager
|
|
* @see \Stancl\Tenancy\Database\TenantDatabaseManagers\SQLiteDatabaseManager
|
|
*/
|
|
trait ValidatesDatabaseParameters
|
|
{
|
|
/**
|
|
* Characters allowed in parameters.
|
|
*
|
|
* Used as the default allowlist in validateParameter(), which validates non-password
|
|
* parameters such as database names or usernames.
|
|
*
|
|
* Since non-password parameters don't need to use as many special characters, we use
|
|
* a stricter allowlist here.
|
|
*/
|
|
public static string $allowedParameterCharacters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-';
|
|
|
|
/**
|
|
* Characters allowed in database user passwords.
|
|
*
|
|
* The allowlist for passwords is less strict than for other parameters
|
|
* because it's more common to use more special characters in passwords.
|
|
*/
|
|
public static string $allowedPasswordCharacters = ' !#$%&()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_abcdefghijklmnopqrstuvwxyz{|}~';
|
|
|
|
/**
|
|
* Ensure that parameter (database name, username, etc.)
|
|
* only contains allowed characters before being used in SQL statements
|
|
* (or paths in the case of SQLiteDatabaseManager).
|
|
*
|
|
* By default, only the characters in $allowedParameterCharacters are allowed.
|
|
*
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
protected function validateParameter(mixed $parameter, string|null $allowedCharacters = null): void
|
|
{
|
|
if (is_null($parameter)) {
|
|
throw new InvalidArgumentException('Parameter cannot be null.');
|
|
}
|
|
|
|
if (is_numeric($parameter)) {
|
|
$parameter = (string) $parameter;
|
|
}
|
|
|
|
if (! is_string($parameter)) {
|
|
throw new InvalidArgumentException('Parameter has to be a string.');
|
|
}
|
|
|
|
if ($parameter === '') {
|
|
throw new InvalidArgumentException('Parameter cannot be an empty string.');
|
|
}
|
|
|
|
$allowedCharacters ??= static::$allowedParameterCharacters;
|
|
|
|
foreach (str_split($parameter) as $character) {
|
|
if (! str_contains($allowedCharacters, $character)) {
|
|
throw new InvalidArgumentException("Forbidden character '{$character}' in parameter.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure password only contains allowed characters ($allowedPasswordCharacters)
|
|
* before being used in SQL statements.
|
|
*
|
|
* Used in permission controlled managers as a shorthand for calling validateParameter()
|
|
* with the less strict allowlist to validate database user passwords.
|
|
*
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
protected function validatePassword(string|null $password): void
|
|
{
|
|
if (is_null($password)) {
|
|
throw new InvalidArgumentException('Password cannot be null.');
|
|
}
|
|
|
|
if ($password === '') {
|
|
throw new InvalidArgumentException('Password cannot be an empty string.');
|
|
}
|
|
|
|
$this->validateParameter($password, allowedCharacters: static::$allowedPasswordCharacters);
|
|
}
|
|
}
|