1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2025-12-12 11:14: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

@ -8,6 +8,7 @@ use Illuminate\Database\QueryException;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Validator;
use Stancl\Tenancy\Database\Models\Tenant;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
use Stancl\Tenancy\Database\Concerns\BelongsToPrimaryModel;
use Stancl\Tenancy\Database\Concerns\HasScopedValidationRules;
@ -40,12 +41,12 @@ test('primary models are scoped to the current tenant', function () {
'id' => 'acme',
]));
$post = Post::create(['text' => 'Foo']);
$post = SingleDatabasePost::create(['text' => 'Foo']);
expect($post->tenant_id)->toBe('acme');
expect($post->tenant->id)->toBe('acme');
$post = Post::first();
$post = SingleDatabasePost::first();
expect($post->tenant_id)->toBe('acme');
expect($post->tenant->id)->toBe('acme');
@ -56,12 +57,12 @@ test('primary models are scoped to the current tenant', function () {
'id' => 'foobar',
]));
$post = Post::create(['text' => 'Bar']);
$post = SingleDatabasePost::create(['text' => 'Bar']);
expect($post->tenant_id)->toBe('foobar');
expect($post->tenant->id)->toBe('foobar');
$post = Post::first();
$post = SingleDatabasePost::first();
expect($post->tenant_id)->toBe('foobar');
expect($post->tenant->id)->toBe('foobar');
@ -71,17 +72,17 @@ test('primary models are scoped to the current tenant', function () {
tenancy()->initialize($acme);
$post = Post::first();
$post = SingleDatabasePost::first();
expect($post->tenant_id)->toBe('acme');
expect($post->tenant->id)->toBe('acme');
// Assert foobar models are inaccessible in acme context
expect(Post::count())->toBe(1);
expect(SingleDatabasePost::count())->toBe(1);
// Primary models are not scoped in the central context
tenancy()->end();
expect(Post::count())->toBe(2);
expect(SingleDatabasePost::count())->toBe(2);
});
test('secondary models ARE scoped to the current tenant when accessed directly and parent relationship trait is used', function () {
@ -90,10 +91,10 @@ test('secondary models ARE scoped to the current tenant when accessed directly a
]);
$acme->run(function () {
$post = Post::create(['text' => 'Foo']);
$post->scoped_comments()->create(['text' => 'Comment Text']);
$post = SingleDatabasePost::create(['text' => 'Foo']);
$post->comments()->create(['text' => 'Comment Text']);
expect(Post::count())->toBe(1);
expect(SingleDatabasePost::count())->toBe(1);
expect(ScopedComment::count())->toBe(1);
});
@ -102,14 +103,16 @@ test('secondary models ARE scoped to the current tenant when accessed directly a
]);
$foobar->run(function () {
expect(Post::count())->toBe(0);
expect(SingleDatabasePost::count())->toBe(0);
expect(ScopedComment::count())->toBe(0);
$post = Post::create(['text' => 'Bar']);
$post->scoped_comments()->create(['text' => 'Comment Text 2']);
$post = SingleDatabasePost::create(['text' => 'Bar']);
$post->comments()->create(['text' => 'Comment Text 2']);
expect(Post::count())->toBe(1);
expect(SingleDatabasePost::count())->toBe(1);
expect(ScopedComment::count())->toBe(1);
// whereas...
expect(Comment::count())->toBe(2);
});
// Global context
@ -123,7 +126,7 @@ test('secondary models are scoped correctly', function () {
'id' => 'acme',
]));
$post = Post::create(['text' => 'Foo']);
$post = SingleDatabasePost::create(['text' => 'Foo']);
$post->comments()->create(['text' => 'Comment text']);
// ================
@ -132,24 +135,24 @@ test('secondary models are scoped correctly', function () {
'id' => 'foobar',
]));
$post = Post::create(['text' => 'Bar']);
$post = SingleDatabasePost::create(['text' => 'Bar']);
$post->comments()->create(['text' => 'Comment text 2']);
// ================
// acme context again
tenancy()->initialize($acme);
expect(Post::count())->toBe(1);
expect(Post::first()->comments->count())->toBe(1);
expect(SingleDatabasePost::count())->toBe(1);
expect(SingleDatabasePost::first()->comments->count())->toBe(1);
// Secondary models are not scoped to the current tenant when accessed directly
expect(tenant('id'))->toBe('acme');
expect(Comment::count())->toBe(2);
expect(BaseComment::count())->toBe(2);
// secondary models are not scoped in the central context
tenancy()->end();
expect(Comment::count())->toBe(2);
expect(BaseComment::count())->toBe(2);
});
test('global models are not scoped at all', function () {
@ -180,7 +183,7 @@ test('tenant id and relationship is auto added when creating primary resources i
'id' => 'acme',
]));
$post = Post::create(['text' => 'Foo']);
$post = SingleDatabasePost::create(['text' => 'Foo']);
expect($post->tenant_id)->toBe('acme');
expect($post->relationLoaded('tenant'))->toBeTrue();
@ -191,7 +194,7 @@ test('tenant id and relationship is auto added when creating primary resources i
test('tenant id is not auto added when creating primary resources in central context', function () {
pest()->expectException(QueryException::class);
Post::create(['text' => 'Foo']);
SingleDatabasePost::create(['text' => 'Foo']);
});
test('tenant id column name can be customized', function () {
@ -214,7 +217,7 @@ test('tenant id column name can be customized', function () {
tenancy()->initialize($acme);
$post = Post::create(['text' => 'Foo']);
$post = SingleDatabasePost::create(['text' => 'Foo']);
expect($post->team_id)->toBe('acme');
@ -224,11 +227,11 @@ test('tenant id column name can be customized', function () {
'id' => 'foobar',
]));
$post = Post::create(['text' => 'Bar']);
$post = SingleDatabasePost::create(['text' => 'Bar']);
expect($post->team_id)->toBe('foobar');
$post = Post::first();
$post = SingleDatabasePost::first();
expect($post->team_id)->toBe('foobar');
@ -237,11 +240,11 @@ test('tenant id column name can be customized', function () {
tenancy()->initialize($acme);
$post = Post::first();
$post = SingleDatabasePost::first();
expect($post->team_id)->toBe('acme');
// Assert foobar models are inaccessible in acme context
expect(Post::count())->toBe(1);
expect(SingleDatabasePost::count())->toBe(1);
});
test('the model returned by the tenant helper has unique and exists validation rules', function () {
@ -254,7 +257,7 @@ test('the model returned by the tenant helper has unique and exists validation r
'id' => 'acme',
]));
Post::create(['text' => 'Foo', 'slug' => 'foo']);
SingleDatabasePost::create(['text' => 'Foo', 'slug' => 'foo']);
$data = ['text' => 'Foo 2', 'slug' => 'foo'];
$uniqueFails = Validator::make($data, [
@ -285,38 +288,40 @@ class SingleDatabaseTenant extends Tenant
use HasScopedValidationRules;
}
class Post extends Model
class SingleDatabasePost extends Model
{
use BelongsToTenant;
protected $guarded = [];
public $table = 'posts';
public $timestamps = false;
public function comments()
{
return $this->hasMany(Comment::class);
}
public function scoped_comments()
{
return $this->hasMany(Comment::class);
return $this->hasMany(BaseComment::class, 'post_id');
}
}
class Comment extends Model
class BaseComment extends Model
{
protected $guarded = [];
protected $table = 'comments';
public $timestamps = false;
public function post()
{
return $this->belongsTo(Post::class);
return $this->belongsTo(SingleDatabasePost::class);
}
}
class ScopedComment extends Comment
// accessed via the comments() relationship (same table as BaseComment)
// however, when used directly, the model scopes queries to the current tenant
// unlike BaseComment
class ScopedComment extends BaseComment
{
use BelongsToPrimaryModel;
@ -326,6 +331,11 @@ class ScopedComment extends Comment
{
return 'post';
}
public function post(): BelongsTo
{
return $this->belongsTo(SingleDatabasePost::class);
}
}
class GlobalResource extends Model