diff --git a/assets/config.php b/assets/config.php index 508b79d9..aea10c5e 100644 --- a/assets/config.php +++ b/assets/config.php @@ -108,8 +108,6 @@ return [ */ 'prefix' => 'tenant', 'suffix' => '', - - 'separate_by' => 'database', // database or schema (only supported by pgsql) ], /** diff --git a/src/Contracts/ManagesDatabaseUsers.php b/src/Contracts/ManagesDatabaseUsers.php new file mode 100644 index 00000000..e369537b --- /dev/null +++ b/src/Contracts/ManagesDatabaseUsers.php @@ -0,0 +1,11 @@ +createTenantConnection($tenant->getDatabaseName(), $tenant->getConnectionName()); + $this->createTenantConnection($tenant); $this->setDefaultConnection($tenant->getConnectionName()); $this->switchConnection($tenant->getConnectionName()); } @@ -90,21 +91,21 @@ class DatabaseManager /** * Create the tenant database connection. * - * @param string $databaseName - * @param string $connectionName + * @param Tenant $tenant * @return void + * @throws DatabaseManagerNotRegisteredException */ - public function createTenantConnection($databaseName, $connectionName) + public function createTenantConnection(Tenant $tenant) { - // Create the database connection. - $based_on = $this->getBaseConnection($connectionName); - $this->app['config']["database.connections.$connectionName"] = $this->app['config']['database.connections.' . $based_on]; + $configuration = $this->getTenantDatabaseManager($tenant) + ->createDatabaseConnection( + $tenant, + $this->getBaseConnectionConfiguration($tenant) + ); - // Change database name. - $databaseName = $this->getDriver($connectionName) === 'sqlite' ? database_path($databaseName) : $databaseName; - $separateBy = $this->separateBy($connectionName); + $connectionName = $tenant->getConnectionName(); - $this->app['config']["database.connections.$connectionName.$separateBy"] = $databaseName; + $this->app['config']->set("database.connections.{$connectionName}", $configuration); } /** @@ -188,9 +189,9 @@ class DatabaseManager } } - QueuedTenantDatabaseCreator::withChain($chain)->dispatch($manager, $database); + QueuedTenantDatabaseCreator::withChain($chain)->dispatch($manager, $database, $tenant); } else { - $manager->createDatabase($database); + $manager->createDatabase($database, $tenant); foreach ($afterCreating as $item) { if (is_object($item) && ! $item instanceof Closure) { $item->handle($tenant); @@ -253,18 +254,15 @@ class DatabaseManager } /** - * What key on the connection config should be used to separate tenants. + * Get the connection base configuration for a tenant * - * @param string $connectionName - * @return string + * @param Tenant $tenant + * @return array */ - public function separateBy(string $connectionName): string + protected function getBaseConnectionConfiguration(Tenant $tenant): array { - if ($this->getDriver($this->getBaseConnection($connectionName)) === 'pgsql' - && $this->app['config']['tenancy.database.separate_by'] === 'schema') { - return 'schema'; - } + $basedOn = $this->getBaseConnection($tenant->getConnectionName()); - return 'database'; + return $this->app['config']->get("database.connections.{$basedOn}"); } } diff --git a/src/Jobs/QueuedTenantDatabaseCreator.php b/src/Jobs/QueuedTenantDatabaseCreator.php index bd03fc55..af7f4b79 100644 --- a/src/Jobs/QueuedTenantDatabaseCreator.php +++ b/src/Jobs/QueuedTenantDatabaseCreator.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class QueuedTenantDatabaseCreator implements ShouldQueue { @@ -21,17 +22,21 @@ class QueuedTenantDatabaseCreator implements ShouldQueue /** @var string */ protected $databaseName; + /** @var Tenant */ + public $tenant; + /** * Create a new job instance. * * @param TenantDatabaseManager $databaseManager * @param string $databaseName - * @return void + * @param Tenant $tenant */ - public function __construct(TenantDatabaseManager $databaseManager, string $databaseName) + public function __construct(TenantDatabaseManager $databaseManager, string $databaseName, Tenant $tenant) { $this->databaseManager = $databaseManager; $this->databaseName = $databaseName; + $this->tenant = $tenant; } /** @@ -41,6 +46,6 @@ class QueuedTenantDatabaseCreator implements ShouldQueue */ public function handle() { - $this->databaseManager->createDatabase($this->databaseName); + $this->databaseManager->createDatabase($this->databaseName, $this->tenant); } } diff --git a/src/Tenant.php b/src/Tenant.php index 38ac7261..2670fafb 100644 --- a/src/Tenant.php +++ b/src/Tenant.php @@ -15,6 +15,7 @@ use Stancl\Tenancy\Contracts\StorageDriver; use Stancl\Tenancy\Contracts\UniqueIdentifierGenerator; use Stancl\Tenancy\Exceptions\NotImplementedException; use Stancl\Tenancy\Exceptions\TenantStorageException; +use Stancl\Tenancy\Traits\ProvidesDatabaseUser; /** * @internal Class is subject to breaking changes in minor and patch versions. @@ -22,7 +23,8 @@ use Stancl\Tenancy\Exceptions\TenantStorageException; class Tenant implements ArrayAccess { use Traits\HasArrayAccess, - ForwardsCalls; + ForwardsCalls, + ProvidesDatabaseUser; /** * Tenant data. A "cache" of tenant storage. diff --git a/src/TenantDatabaseManagers/MySQLDatabaseManager.php b/src/TenantDatabaseManagers/MySQLDatabaseManager.php index f6c4ef96..ecf517f6 100644 --- a/src/TenantDatabaseManagers/MySQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/MySQLDatabaseManager.php @@ -9,6 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { @@ -30,7 +31,7 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection $this->connection = $connection; } - public function createDatabase(string $name): bool + public function createDatabase(string $name, Tenant $tenant): bool { $charset = $this->database()->getConfig('charset'); $collation = $this->database()->getConfig('collation'); @@ -47,4 +48,14 @@ class MySQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { return (bool) $this->database()->select("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$name'"); } + + /** + * @inheritDoc + */ + public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array + { + return array_replace_recursive($baseConfiguration, [ + 'database' => $tenant->getDatabaseName(), + ]); + } } diff --git a/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php new file mode 100644 index 00000000..14708d33 --- /dev/null +++ b/src/TenantDatabaseManagers/PermissionControlledMySQLDatabaseManager.php @@ -0,0 +1,64 @@ +createDatabaseUser($name, $tenant); + + return true; + } + + public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array + { + return array_replace_recursive( + parent::createDatabaseConnection($tenant, $baseConfiguration), + array_filter([ + 'host' => $tenant->getDatabaseHost(), + 'username' => $tenant->getDatabaseUsername(), + 'password' => $tenant->getDatabasePassword(), + 'port' => $tenant->getDatabasePort(), + 'url' => $tenant->getDatabaseUrl() + ]) + ); + } + + public function createDatabaseUser(string $databaseName, Tenant $tenant): void + { + $username = $tenant->generateDatabaseUsername(); + $password = $tenant->generateDatabasePassword(); + $appHost = $tenant->getDatabaseHost() ?? $this->getBaseConfigurationFor('host'); + + $grants = implode(', ', $tenant->getDatabaseGrants()); + + $this->database()->statement( + "CREATE USER '$username'@'$appHost' IDENTIFIED BY '$password" + ); + + $this->database()->statement( + "GRANT $grants ON $databaseName.* TO '$username'@'$appHost' IDENTIFIED BY '$password'" + ); + + $tenant->withData([ + '_tenancy_db_username' => $username, + '_tenancy_db_password' => $password, + '_tenancy_db_host' => $appHost, + '_tenancy_db_link' => $tenant->getDatabaseLink() + ])->save(); + } + + private function getBaseConfigurationFor(string $key) + { + return $this->database()->getConfig($key); + } +} diff --git a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php index fc21668e..e7b4cabf 100644 --- a/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLDatabaseManager.php @@ -9,6 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnection { @@ -30,7 +31,7 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti $this->connection = $connection; } - public function createDatabase(string $name): bool + public function createDatabase(string $name, Tenant $tenant): bool { return $this->database()->statement("CREATE DATABASE \"$name\" WITH TEMPLATE=template0"); } @@ -44,4 +45,18 @@ class PostgreSQLDatabaseManager implements TenantDatabaseManager, CanSetConnecti { return (bool) $this->database()->select("SELECT datname FROM pg_database WHERE datname = '$name'"); } + + /** + * @inheritDoc + */ + public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array + { + if ('pgsql' !== $baseConfiguration['driver']) { + throw new \Exception('Mismatching driver for tenant'); + } + + return array_replace_recursive($baseConfiguration, [ + 'database' => $tenant->getDatabaseName() + ]); + } } diff --git a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php index a93ed901..cf29caec 100644 --- a/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php +++ b/src/TenantDatabaseManagers/PostgreSQLSchemaManager.php @@ -9,6 +9,7 @@ use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Stancl\Tenancy\Contracts\Future\CanSetConnection; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection { @@ -30,7 +31,7 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection $this->connection = $connection; } - public function createDatabase(string $name): bool + public function createDatabase(string $name, Tenant $tenant): bool { return $this->database()->statement("CREATE SCHEMA \"$name\""); } @@ -44,4 +45,15 @@ class PostgreSQLSchemaManager implements TenantDatabaseManager, CanSetConnection { return (bool) $this->database()->select("SELECT schema_name FROM information_schema.schemata WHERE schema_name = '$name'"); } + + public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array + { + if ('pgsql' !== $baseConfiguration['driver']) { + throw new \Exception('Mismatching driver for tenant'); + } + + return array_replace_recursive($baseConfiguration, [ + 'schema' => $tenant->getDatabaseName() + ]); + } } diff --git a/src/TenantDatabaseManagers/SQLiteDatabaseManager.php b/src/TenantDatabaseManagers/SQLiteDatabaseManager.php index 5d681ded..5e959a82 100644 --- a/src/TenantDatabaseManagers/SQLiteDatabaseManager.php +++ b/src/TenantDatabaseManagers/SQLiteDatabaseManager.php @@ -5,10 +5,11 @@ declare(strict_types=1); namespace Stancl\Tenancy\TenantDatabaseManagers; use Stancl\Tenancy\Contracts\TenantDatabaseManager; +use Stancl\Tenancy\Tenant; class SQLiteDatabaseManager implements TenantDatabaseManager { - public function createDatabase(string $name): bool + public function createDatabase(string $name, Tenant $tenant): bool { try { return fclose(fopen(database_path($name), 'w')); @@ -30,4 +31,11 @@ class SQLiteDatabaseManager implements TenantDatabaseManager { return file_exists(database_path($name)); } + + public function createDatabaseConnection(Tenant $tenant, array $baseConfiguration): array + { + return array_replace_recursive($baseConfiguration, [ + 'database' => database_path($tenant->getDatabaseName()) + ]); + } } diff --git a/src/Traits/ProvidesDatabaseUser.php b/src/Traits/ProvidesDatabaseUser.php new file mode 100644 index 00000000..429fdbed --- /dev/null +++ b/src/Traits/ProvidesDatabaseUser.php @@ -0,0 +1,54 @@ +data['_tenancy_db_host']; + } + + public function getDatabaseUrl(): ?string + { + return $this->data['_tenancy_db_url']; + } + + public function getDatabaseUsername(): string + { + return $this->data['_tenancy_db_username']; + } + + public function getDatabasePassword(): string + { + return $this->data['_tenancy_db_password']; + } + + public function getDatabasePort(): ?string + { + return $this->data['_tenancy_db_port'] ?? null; + } + + public function getDatabaseGrants(): array + { + return $this->data['_tenancy_db_grants'] ?? $this->config['tenancy.database.grants']; + } + + public function generateDatabaseUsername(): string + { + return Str::random(16); + } + + public function generateDatabasePassword(): string + { + return Hash::make(Str::random(16)); + } + + public function getDatabaseLink(): ?string + { + return $this->data['_tenancy_db_link'] ?? null; + } +}