1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-05-07 16:24:04 +00:00

Postgres RLS + permission controlled database managers (#33)

This PR adds Postgres RLS (trait manager + table manager approach) and permission controlled managers for PostgreSQL.

---------

Co-authored-by: lukinovec <lukinovec@gmail.com>
Co-authored-by: PHP CS Fixer <phpcsfixer@example.com>
This commit is contained in:
Samuel Štancl 2024-04-24 22:32:49 +02:00 committed by GitHub
parent 34297d3e1a
commit 7317d2638a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 2511 additions and 112 deletions

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
use Stancl\Tenancy\Database\ParentModelScope;
use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager;
trait BelongsToPrimaryModel
{
@ -12,6 +13,10 @@ trait BelongsToPrimaryModel
public static function bootBelongsToPrimaryModel(): void
{
static::addGlobalScope(new ParentModelScope);
$implicitRLS = config('tenancy.rls.manager') === TraitRLSManager::class && TraitRLSManager::$implicitRLS;
if (! $implicitRLS && ! (new static) instanceof RLSModel) {
static::addGlobalScope(new ParentModelScope);
}
}
}

View file

@ -7,6 +7,7 @@ namespace Stancl\Tenancy\Database\Concerns;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Database\TenantScope;
use Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager;
use Stancl\Tenancy\Tenancy;
/**
@ -14,6 +15,8 @@ use Stancl\Tenancy\Tenancy;
*/
trait BelongsToTenant
{
use FillsCurrentTenant;
public function tenant(): BelongsTo
{
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
@ -21,15 +24,12 @@ trait BelongsToTenant
public static function bootBelongsToTenant(): void
{
static::addGlobalScope(new TenantScope);
// If TraitRLSManager::$implicitRLS is true or this model implements RLSModel
// Postgres RLS is used for scoping, so we don't enable the scope used with single-database tenancy.
$implicitRLS = config('tenancy.rls.manager') === TraitRLSManager::class && TraitRLSManager::$implicitRLS;
static::creating(function ($model) {
if (! $model->getAttribute(Tenancy::tenantKeyColumn()) && ! $model->relationLoaded('tenant')) {
if (tenancy()->initialized) {
$model->setAttribute(Tenancy::tenantKeyColumn(), tenant()->getTenantKey());
$model->setRelation('tenant', tenant());
}
}
});
if (! $implicitRLS && ! (new static) instanceof RLSModel) {
static::addGlobalScope(new TenantScope);
}
}
}

View file

@ -17,8 +17,9 @@ trait CreatesDatabaseUsers
public function deleteDatabase(TenantWithDatabase $tenant): bool
{
parent::deleteDatabase($tenant);
// Some DB engines require the user to be deleted before the database (e.g. Postgres)
$this->deleteUser($tenant->database());
return $this->deleteUser($tenant->database());
return parent::deleteDatabase($tenant);
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
use Stancl\Tenancy\Tenancy;
trait FillsCurrentTenant
{
public static function bootFillsCurrentTenant(): void
{
static::creating(function ($model) {
if (! $model->getAttribute(Tenancy::tenantKeyColumn()) && ! $model->relationLoaded('tenant')) {
if (tenancy()->initialized) {
$model->setAttribute(Tenancy::tenantKeyColumn(), tenant()->getTenantKey());
$model->setRelation('tenant', tenant());
}
}
});
}
}

View file

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
use Stancl\Tenancy\Database\DatabaseConfig;
use Stancl\Tenancy\Database\TenantDatabaseManagers\TenantDatabaseManager;
/**
* @method \Illuminate\Database\Connection database()
* @mixin TenantDatabaseManager
*/
trait ManagesPostgresUsers
{
/**
* Grant database/schema and table permissions to the user whose credentials are stored in the passed DatabaseConfig.
*
* With schema manager, the schema name is stored in the 'search_path' key of the connection config,
* but it's still accessible using $databaseConfig->getName().
*/
abstract protected function grantPermissions(DatabaseConfig $databaseConfig): bool;
public function createUser(DatabaseConfig $databaseConfig): bool
{
/** @var string $username */
$username = $databaseConfig->getUsername();
$password = $databaseConfig->getPassword();
$createUser = ! $this->userExists($username);
if ($createUser) {
$this->database()->statement("CREATE USER \"{$username}\" LOGIN PASSWORD '{$password}'");
}
$this->grantPermissions($databaseConfig);
return $createUser;
}
public function deleteUser(DatabaseConfig $databaseConfig): bool
{
/** @var TenantDatabaseManager $this */
// Tenant DB username
$username = $databaseConfig->getUsername();
// Tenant host connection config
$connectionName = $this->database()->getConfig('name');
$centralDatabase = $this->database()->getConfig('database');
// Set the DB/schema name to the tenant DB/schema name
config()->set(
"database.connections.{$connectionName}",
$this->makeConnectionConfig($this->database()->getConfig(), $databaseConfig->getName()),
);
// Connect to the tenant DB/schema
$this->database()->reconnect();
// Delete all database objects owned by the user (privileges, tables, views, etc.)
// Postgres users cannot be deleted unless we delete all objects owned by it first
$this->database()->statement("DROP OWNED BY \"{$username}\"");
// Delete the user
$userDeleted = $this->database()->statement("DROP USER \"{$username}\"");
config()->set(
"database.connections.{$connectionName}",
$this->makeConnectionConfig($this->database()->getConfig(), $centralDatabase),
);
// Reconnect to the central database
$this->database()->reconnect();
return $userDeleted;
}
public function userExists(string $username): bool
{
return (bool) $this->database()->selectOne("SELECT usename FROM pg_user WHERE usename = '{$username}'");
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns;
/**
* Interface indicating that the queries of the model it's used on
* get scoped using RLS (instead of the global TenantScope).
*
* All models whose queries you want to scope using RLS
* need to implement this interface if RLS scoping is explicit (= when TraitRLSManager::$implicitRLS is false).
* The models also have to use one of the single-database traits.
*
* Used with Postgres RLS via TraitRLSManager.
*
* @see \Stancl\Tenancy\RLS\PolicyManagers\TraitRLSManager
* @see BelongsToTenant
* @see BelongsToPrimaryModel
*/
interface RLSModel {}