Multi-tenant Authorization System
Difficulty: Advanced | Time: 2+ hours | Integration: Policies, Custom Gates, Multi-tenancy, DI
Build a sophisticated authorization system supporting multiple tenant types with complex permission hierarchies, role inheritance, and specification-driven access control.
The Challenge: Complex Multi-tenant Authorization
Your SaaS application serves multiple tenant types (organizations, teams, personal accounts) with complex permission structures, role hierarchies, and resource-specific access rules that traditional Laravel authorization cannot handle elegantly.
🔍 The Problem: Authorization Spaghetti
Here's the nightmarish authorization code scattered across your application:
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Models\Document;
use App\Models\Tenant;
use Illuminate\Http\Request;
class DocumentController extends Controller
{
public function show(Request $request, Document $document)
{
$user = $request->user();
$tenant = $request->tenant(); // Custom middleware sets this
// Complex authorization logic mixed with business logic
if (!$tenant) {
abort(403, 'No tenant context');
}
// Check if user belongs to tenant
$userTenant = $user->tenants()
->where('tenant_id', $tenant->id)
->first();
if (!$userTenant) {
abort(403, 'User not member of tenant');
}
// Check if document belongs to tenant
if ($document->tenant_id !== $tenant->id) {
abort(403, 'Document not in tenant');
}
// Role-based permissions (getting complex!)
$userRole = $userTenant->pivot->role;
if ($document->is_confidential) {
// Only admins and managers can see confidential docs
if (!in_array($userRole, ['admin', 'manager'])) {
abort(403, 'Insufficient permissions for confidential document');
}
// But in enterprise tenants, senior members can also access
if ($tenant->type === 'enterprise' && $userRole === 'senior_member') {
// Allow access
} elseif ($tenant->type === 'enterprise' && $userRole !== 'admin' && $userRole !== 'manager') {
abort(403, 'Enterprise confidential access denied');
}
}
// Document status permissions
if ($document->status === 'draft') {
// Only document owner or admins can see drafts
if ($document->created_by !== $user->id && $userRole !== 'admin') {
abort(403, 'Cannot access draft document');
}
}
// Time-based access (getting ridiculous!)
if ($document->access_expires_at && $document->access_expires_at < now()) {
if ($userRole !== 'admin') {
abort(403, 'Document access has expired');
}
}
// Department-based access for enterprise tenants
if ($tenant->type === 'enterprise' && $document->department_id) {
$userDepartments = $user->departments()
->where('tenant_id', $tenant->id)
->pluck('department_id')
->toArray();
if (!in_array($document->department_id, $userDepartments) && $userRole !== 'admin') {
abort(403, 'Department access denied');
}
}
return view('documents.show', compact('document'));
}
public function update(Request $request, Document $document)
{
// DUPLICATE authorization logic again! 😱
$user = $request->user();
$tenant = $request->tenant();
// ... repeat all the same logic with slight variations for editing
if ($document->is_locked && !in_array($userRole, ['admin'])) {
abort(403, 'Document is locked');
}
// ... more duplicate code
}
public function delete(Request $request, Document $document)
{
// EVEN MORE duplicate logic with delete-specific rules
// ... this is getting out of hand
}
}
🎯 Authorization Requirements Analysis
Let's identify the complex authorization rules:
- Tenant Membership: User must belong to the tenant
- Resource Ownership: Resources must belong to the correct tenant
- Role Hierarchies: Admin > Manager > Senior Member > Member > Guest
- Tenant Type Rules: Enterprise tenants have different permissions than Team/Personal
- Resource-Specific Rules: Confidential, draft, expired, locked documents
- Department-Based Access: Enterprise tenants support department restrictions
- Time-Based Access: Temporary access permissions and expiration
- Action-Specific Permissions: View vs Edit vs Delete vs Admin actions
Step 1: Generate Authorization Specifications
Using the Artisan command to create our authorization specifications:
# Core tenant specifications
php artisan make:specification Tenant/TenantMembershipSpecification --model=User
php artisan make:specification Tenant/TenantResourceOwnershipSpecification --model=Resource
php artisan make:specification Tenant/RoleHierarchySpecification --model=User
php artisan make:specification Tenant/TenantTypePermissionSpecification --model=User
# Document-specific specifications
php artisan make:specification Document/ConfidentialDocumentAccessSpecification --model=Document
php artisan make:specification Document/DraftDocumentAccessSpecification --model=Document
php artisan make:specification Document/TimeBasedAccessSpecification --model=Document
php artisan make:specification Document/DepartmentAccessSpecification --model=Document
php artisan make:specification Document/DocumentStatusSpecification --model=Document
# Composite specifications for actions
php artisan make:specification Authorization/DocumentViewSpecification --composite
php artisan make:specification Authorization/DocumentEditSpecification --composite
php artisan make:specification Authorization/DocumentDeleteSpecification --composite
php artisan make:specification Authorization/DocumentAdminSpecification --composite
Step 2: Implement Core Tenant Specifications
TenantMembershipSpecification
<?php
namespace App\Specifications\Tenant;
use App\Models\User;
use App\Models\Tenant;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class TenantMembershipSpecification extends AbstractSpecification
{
public function __construct(
private readonly Tenant $tenant,
private readonly ?array $allowedStatuses = ['active', 'pending']
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof User) {
return false;
}
$membership = $candidate->tenants()
->where('tenant_id', $this->tenant->id)
->first();
if (!$membership) {
return false;
}
return in_array($membership->pivot->status, $this->allowedStatuses);
}
public function toQuery(Builder $query): Builder
{
return $query->whereHas('tenants', function ($q) {
$q->where('tenant_id', $this->tenant->id)
->whereIn('status', $this->allowedStatuses);
});
}
public function getUserRole(User $user): ?string
{
$membership = $user->tenants()
->where('tenant_id', $this->tenant->id)
->first();
return $membership?->pivot->role;
}
public function getMembershipDetails(User $user): ?array
{
$membership = $user->tenants()
->where('tenant_id', $this->tenant->id)
->first();
if (!$membership) {
return null;
}
return [
'role' => $membership->pivot->role,
'status' => $membership->pivot->status,
'joined_at' => $membership->pivot->created_at,
'permissions' => $membership->pivot->permissions ?? [],
];
}
}
RoleHierarchySpecification
<?php
namespace App\Specifications\Tenant;
use App\Models\User;
use App\Models\Tenant;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class RoleHierarchySpecification extends AbstractSpecification
{
private array $roleHierarchy = [
'admin' => 100,
'manager' => 80,
'senior_member' => 60,
'member' => 40,
'guest' => 20,
];
public function __construct(
private readonly Tenant $tenant,
private readonly string $minimumRole
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof User) {
return false;
}
$membershipSpec = new TenantMembershipSpecification($this->tenant);
if (!$membershipSpec->isSatisfiedBy($candidate)) {
return false;
}
$userRole = $membershipSpec->getUserRole($candidate);
return $this->getRoleWeight($userRole) >= $this->getRoleWeight($this->minimumRole);
}
public function toQuery(Builder $query): Builder
{
$minimumWeight = $this->getRoleWeight($this->minimumRole);
$allowedRoles = [];
foreach ($this->roleHierarchy as $role => $weight) {
if ($weight >= $minimumWeight) {
$allowedRoles[] = $role;
}
}
return $query->whereHas('tenants', function ($q) use ($allowedRoles) {
$q->where('tenant_id', $this->tenant->id)
->whereIn('role', $allowedRoles);
});
}
private function getRoleWeight(string $role): int
{
return $this->roleHierarchy[$role] ?? 0;
}
public function hasHigherRole(User $user, string $comparisonRole): bool
{
$membershipSpec = new TenantMembershipSpecification($this->tenant);
$userRole = $membershipSpec->getUserRole($user);
return $this->getRoleWeight($userRole) > $this->getRoleWeight($comparisonRole);
}
public function canDelegate(User $user, string $targetRole): bool
{
$membershipSpec = new TenantMembershipSpecification($this->tenant);
$userRole = $membershipSpec->getUserRole($user);
// Users can only delegate to roles lower than their own
return $this->getRoleWeight($userRole) > $this->getRoleWeight($targetRole);
}
}
TenantTypePermissionSpecification
<?php
namespace App\Specifications\Tenant;
use App\Models\User;
use App\Models\Tenant;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class TenantTypePermissionSpecification extends AbstractSpecification
{
private array $tenantTypePermissions = [
'enterprise' => [
'department_access' => true,
'advanced_reporting' => true,
'bulk_operations' => true,
'api_access' => true,
'custom_roles' => true,
],
'team' => [
'department_access' => false,
'advanced_reporting' => true,
'bulk_operations' => false,
'api_access' => true,
'custom_roles' => false,
],
'personal' => [
'department_access' => false,
'advanced_reporting' => false,
'bulk_operations' => false,
'api_access' => false,
'custom_roles' => false,
],
];
public function __construct(
private readonly Tenant $tenant,
private readonly string $permission
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof User) {
return false;
}
$membershipSpec = new TenantMembershipSpecification($this->tenant);
if (!$membershipSpec->isSatisfiedBy($candidate)) {
return false;
}
return $this->tenantHasPermission($this->permission);
}
public function toQuery(Builder $query): Builder
{
$allowedTenantTypes = [];
foreach ($this->tenantTypePermissions as $type => $permissions) {
if ($permissions[$this->permission] ?? false) {
$allowedTenantTypes[] = $type;
}
}
return $query->whereHas('tenants', function ($q) use ($allowedTenantTypes) {
$q->where('tenant_id', $this->tenant->id)
->whereIn('type', $allowedTenantTypes);
});
}
private function tenantHasPermission(string $permission): bool
{
$tenantType = $this->tenant->type;
return $this->tenantTypePermissions[$tenantType][$permission] ?? false;
}
public function getAllowedPermissions(): array
{
$tenantType = $this->tenant->type;
return array_keys(
array_filter($this->tenantTypePermissions[$tenantType] ?? [])
);
}
}
Step 3: Document-Specific Specifications
ConfidentialDocumentAccessSpecification
<?php
namespace App\Specifications\Document;
use App\Models\User;
use App\Models\Document;
use App\Models\Tenant;
use App\Specifications\Tenant\RoleHierarchySpecification;
use App\Specifications\Tenant\TenantTypePermissionSpecification;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class ConfidentialDocumentAccessSpecification extends AbstractSpecification
{
public function __construct(
private readonly User $user,
private readonly Tenant $tenant
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof Document) {
return false;
}
// Non-confidential documents are accessible to all tenant members
if (!$candidate->is_confidential) {
return true;
}
// Admins and managers always have access
$roleSpec = new RoleHierarchySpecification($this->tenant, 'manager');
if ($roleSpec->isSatisfiedBy($this->user)) {
return true;
}
// Enterprise tenants: senior members can access confidential docs
if ($this->tenant->type === 'enterprise') {
$seniorRoleSpec = new RoleHierarchySpecification($this->tenant, 'senior_member');
if ($seniorRoleSpec->isSatisfiedBy($this->user)) {
return true;
}
}
// Document owner can always access their own confidential documents
if ($candidate->created_by === $this->user->id) {
return true;
}
// Check if user has explicit permission for this document
if ($candidate->explicitPermissions()->where('user_id', $this->user->id)->exists()) {
return true;
}
return false;
}
public function toQuery(Builder $query): Builder
{
return $query->where(function ($q) {
// Non-confidential documents
$q->where('is_confidential', false)
// OR user has sufficient role
->orWhereHas('tenant', function ($tenantQuery) {
// This is complex - in practice, you might handle this at the collection level
$tenantQuery->where('id', $this->tenant->id);
})
// OR user is document owner
->orWhere('created_by', $this->user->id)
// OR user has explicit permission
->orWhereHas('explicitPermissions', function ($permQuery) {
$permQuery->where('user_id', $this->user->id);
});
});
}
public function getAccessReason(Document $document): string
{
if (!$document->is_confidential) {
return 'Document is not confidential';
}
$roleSpec = new RoleHierarchySpecification($this->tenant, 'manager');
if ($roleSpec->isSatisfiedBy($this->user)) {
return 'User has management role';
}
if ($this->tenant->type === 'enterprise') {
$seniorRoleSpec = new RoleHierarchySpecification($this->tenant, 'senior_member');
if ($seniorRoleSpec->isSatisfiedBy($this->user)) {
return 'Senior member in enterprise tenant';
}
}
if ($document->created_by === $this->user->id) {
return 'Document owner';
}
if ($document->explicitPermissions()->where('user_id', $this->user->id)->exists()) {
return 'Explicit permission granted';
}
return 'Access denied';
}
}
DepartmentAccessSpecification
<?php
namespace App\Specifications\Document;
use App\Models\User;
use App\Models\Document;
use App\Models\Tenant;
use App\Specifications\Tenant\RoleHierarchySpecification;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
use Illuminate\Database\Eloquent\Builder;
class DepartmentAccessSpecification extends AbstractSpecification
{
public function __construct(
private readonly User $user,
private readonly Tenant $tenant
) {}
public function isSatisfiedBy(mixed $candidate): bool
{
if (!$candidate instanceof Document) {
return false;
}
// Documents without department restrictions are accessible to all
if (!$candidate->department_id) {
return true;
}
// Only enterprise tenants support departments
if ($this->tenant->type !== 'enterprise') {
return true; // No department restrictions for non-enterprise
}
// Admins bypass department restrictions
$adminSpec = new RoleHierarchySpecification($this->tenant, 'admin');
if ($adminSpec->isSatisfiedBy($this->user)) {
return true;
}
// Check if user belongs to the document's department
$userDepartments = $this->user->departments()
->where('tenant_id', $this->tenant->id)
->pluck('department_id')
->toArray();
return in_array($candidate->department_id, $userDepartments);
}
public function toQuery(Builder $query): Builder
{
// For enterprise tenants with department restrictions
if ($this->tenant->type === 'enterprise') {
$userDepartments = $this->user->departments()
->where('tenant_id', $this->tenant->id)
->pluck('department_id')
->toArray();
return $query->where(function ($q) use ($userDepartments) {
$q->whereNull('department_id') // No department restriction
->orWhereIn('department_id', $userDepartments); // User's departments
});
}
return $query; // No department restrictions for non-enterprise
}
public function getUserDepartments(): array
{
return $this->user->departments()
->where('tenant_id', $this->tenant->id)
->pluck('name', 'department_id')
->toArray();
}
}
Step 4: Composite Authorization Specifications
DocumentViewSpecification
<?php
namespace App\Specifications\Authorization;
use App\Models\User;
use App\Models\Document;
use App\Models\Tenant;
use App\Specifications\Tenant\TenantMembershipSpecification;
use App\Specifications\Tenant\TenantResourceOwnershipSpecification;
use App\Specifications\Document\ConfidentialDocumentAccessSpecification;
use App\Specifications\Document\DraftDocumentAccessSpecification;
use App\Specifications\Document\TimeBasedAccessSpecification;
use App\Specifications\Document\DepartmentAccessSpecification;
use DangerWayne\LaravelSpecifications\AbstractSpecification;
class DocumentViewSpecification extends AbstractSpecification
{
private $compositeSpec;
public function __construct(User $user, Tenant $tenant)
{
// Build composite specification for document viewing
$this->compositeSpec = (new TenantMembershipSpecification($tenant))
->and(new TenantResourceOwnershipSpecification($tenant))
->and(new ConfidentialDocumentAccessSpecification($user, $tenant))
->and(new DraftDocumentAccessSpecification($user, $tenant))
->and(new TimeBasedAccessSpecification($user, $tenant))
->and(new DepartmentAccessSpecification($user, $tenant));
}
public function isSatisfiedBy(mixed $candidate): bool
{
return $this->compositeSpec->isSatisfiedBy($candidate);
}
public function toQuery($query)
{
return $this->compositeSpec->toQuery($query);
}
public function getFailureReasons(User $user, Document $document): array
{
$failures = [];
$specs = [
'membership' => new TenantMembershipSpecification($this->tenant),
'ownership' => new TenantResourceOwnershipSpecification($this->tenant),
'confidential' => new ConfidentialDocumentAccessSpecification($user, $this->tenant),
'draft' => new DraftDocumentAccessSpecification($user, $this->tenant),
'time_based' => new TimeBasedAccessSpecification($user, $this->tenant),
'department' => new DepartmentAccessSpecification($user, $this->tenant),
];
foreach ($specs as $name => $spec) {
if (!$spec->isSatisfiedBy($document)) {
$failures[] = $name;
}
}
return $failures;
}
}
Step 5: Custom Gate Integration
Register specifications as Laravel Gates:
<?php
namespace App\Providers;
use App\Models\User;
use App\Models\Document;
use App\Models\Tenant;
use App\Specifications\Authorization\DocumentViewSpecification;
use App\Specifications\Authorization\DocumentEditSpecification;
use App\Specifications\Authorization\DocumentDeleteSpecification;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
public function boot(): void
{
Gate::define('view-document', function (User $user, Document $document, ?Tenant $tenant = null) {
$tenant = $tenant ?? $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$spec = new DocumentViewSpecification($user, $tenant);
return $spec->isSatisfiedBy($document);
});
Gate::define('edit-document', function (User $user, Document $document, ?Tenant $tenant = null) {
$tenant = $tenant ?? $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$spec = new DocumentEditSpecification($user, $tenant);
return $spec->isSatisfiedBy($document);
});
Gate::define('delete-document', function (User $user, Document $document, ?Tenant $tenant = null) {
$tenant = $tenant ?? $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$spec = new DocumentDeleteSpecification($user, $tenant);
return $spec->isSatisfiedBy($document);
});
// Dynamic permission checking
Gate::define('tenant-permission', function (User $user, string $permission, ?Tenant $tenant = null) {
$tenant = $tenant ?? $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$spec = new TenantTypePermissionSpecification($tenant, $permission);
return $spec->isSatisfiedBy($user);
});
}
private function getCurrentTenant(): ?Tenant
{
return app('current.tenant');
}
}
Step 6: Policy Integration
Create a comprehensive document policy:
<?php
namespace App\Policies;
use App\Models\User;
use App\Models\Document;
use App\Models\Tenant;
use App\Specifications\Authorization\DocumentViewSpecification;
use App\Specifications\Authorization\DocumentEditSpecification;
use App\Specifications\Authorization\DocumentDeleteSpecification;
class DocumentPolicy
{
public function __construct()
{
// Dependencies injected automatically by Laravel
}
public function view(User $user, Document $document): bool
{
$tenant = $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$spec = new DocumentViewSpecification($user, $tenant);
return $spec->isSatisfiedBy($document);
}
public function update(User $user, Document $document): bool
{
$tenant = $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$spec = new DocumentEditSpecification($user, $tenant);
return $spec->isSatisfiedBy($document);
}
public function delete(User $user, Document $document): bool
{
$tenant = $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$spec = new DocumentDeleteSpecification($user, $tenant);
return $spec->isSatisfiedBy($document);
}
public function viewAny(User $user): bool
{
$tenant = $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$membershipSpec = new TenantMembershipSpecification($tenant);
return $membershipSpec->isSatisfiedBy($user);
}
public function create(User $user): bool
{
$tenant = $this->getCurrentTenant();
if (!$tenant) {
return false;
}
$roleSpec = new RoleHierarchySpecification($tenant, 'member');
return $roleSpec->isSatisfiedBy($user);
}
private function getCurrentTenant(): ?Tenant
{
return app('current.tenant');
}
}
Step 7: Clean Controller Implementation
Now your controller becomes incredibly clean:
<?php
namespace App\Http\Controllers;
use App\Models\Document;
use Illuminate\Http\Request;
class DocumentController extends Controller
{
public function show(Request $request, Document $document)
{
// Authorization handled by policy/gate
$this->authorize('view', $document);
return view('documents.show', compact('document'));
}
public function update(Request $request, Document $document)
{
$this->authorize('update', $document);
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$document->update($validated);
return redirect()
->route('documents.show', $document)
->with('success', 'Document updated successfully');
}
public function destroy(Request $request, Document $document)
{
$this->authorize('delete', $document);
$document->delete();
return redirect()
->route('documents.index')
->with('success', 'Document deleted successfully');
}
// Bulk operations with specification-powered queries
public function index(Request $request)
{
$this->authorize('viewAny', Document::class);
$user = $request->user();
$tenant = app('current.tenant');
// Use specification to filter documents the user can access
$spec = new DocumentViewSpecification($user, $tenant);
$documents = $spec->toQuery(Document::query())
->with(['creator', 'department'])
->paginate(20);
return view('documents.index', compact('documents'));
}
}
Step 8: Advanced Testing
Integration Testing
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Models\Document;
use App\Models\Tenant;
use App\Models\Department;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class MultiTenantAuthorizationTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function enterprise_senior_members_can_access_confidential_documents()
{
// Setup enterprise tenant
$tenant = Tenant::factory()->create(['type' => 'enterprise']);
// Create departments
$hrDept = Department::factory()->create(['tenant_id' => $tenant->id, 'name' => 'HR']);
$itDept = Department::factory()->create(['tenant_id' => $tenant->id, 'name' => 'IT']);
// Create users with different roles
$admin = User::factory()->create();
$seniorMember = User::factory()->create();
$member = User::factory()->create();
// Setup tenant memberships
$admin->tenants()->attach($tenant->id, ['role' => 'admin', 'status' => 'active']);
$seniorMember->tenants()->attach($tenant->id, ['role' => 'senior_member', 'status' => 'active']);
$member->tenants()->attach($tenant->id, ['role' => 'member', 'status' => 'active']);
// Setup department memberships
$seniorMember->departments()->attach($hrDept->id);
// Create confidential document in HR department
$confidentialDoc = Document::factory()->create([
'tenant_id' => $tenant->id,
'department_id' => $hrDept->id,
'is_confidential' => true,
'created_by' => $admin->id,
]);
// Test access
$this->actingAs($admin);
$response = $this->get(route('documents.show', $confidentialDoc));
$response->assertOk(); // Admin can access
$this->actingAs($seniorMember);
$response = $this->get(route('documents.show', $confidentialDoc));
$response->assertOk(); // Senior member in enterprise can access confidential docs
$this->actingAs($member);
$response = $this->get(route('documents.show', $confidentialDoc));
$response->assertForbidden(); // Regular member cannot access
}
/** @test */
public function team_tenant_senior_members_cannot_access_confidential_documents()
{
// Setup team tenant (not enterprise)
$tenant = Tenant::factory()->create(['type' => 'team']);
$seniorMember = User::factory()->create();
$seniorMember->tenants()->attach($tenant->id, ['role' => 'senior_member', 'status' => 'active']);
$confidentialDoc = Document::factory()->create([
'tenant_id' => $tenant->id,
'is_confidential' => true,
]);
$this->actingAs($seniorMember);
$response = $this->get(route('documents.show', $confidentialDoc));
$response->assertForbidden(); // Senior member in team tenant cannot access confidential docs
}
/** @test */
public function department_restrictions_work_correctly()
{
$tenant = Tenant::factory()->create(['type' => 'enterprise']);
$hrDept = Department::factory()->create(['tenant_id' => $tenant->id]);
$itDept = Department::factory()->create(['tenant_id' => $tenant->id]);
$hrMember = User::factory()->create();
$hrMember->tenants()->attach($tenant->id, ['role' => 'member', 'status' => 'active']);
$hrMember->departments()->attach($hrDept->id);
$hrDoc = Document::factory()->create([
'tenant_id' => $tenant->id,
'department_id' => $hrDept->id,
]);
$itDoc = Document::factory()->create([
'tenant_id' => $tenant->id,
'department_id' => $itDept->id,
]);
$this->actingAs($hrMember);
$response = $this->get(route('documents.show', $hrDoc));
$response->assertOk(); // Can access HR document
$response = $this->get(route('documents.show', $itDoc));
$response->assertForbidden(); // Cannot access IT document
}
}
Specification Unit Tests
<?php
namespace Tests\Unit\Specifications;
use App\Models\User;
use App\Models\Tenant;
use App\Models\Document;
use App\Specifications\Tenant\RoleHierarchySpecification;
use App\Specifications\Document\ConfidentialDocumentAccessSpecification;
use Tests\TestCase;
class TenantAuthorizationSpecificationTest extends TestCase
{
/** @test */
public function role_hierarchy_specification_validates_permissions_correctly()
{
$tenant = Tenant::factory()->create();
$admin = User::factory()->create();
$manager = User::factory()->create();
$member = User::factory()->create();
$admin->tenants()->attach($tenant->id, ['role' => 'admin', 'status' => 'active']);
$manager->tenants()->attach($tenant->id, ['role' => 'manager', 'status' => 'active']);
$member->tenants()->attach($tenant->id, ['role' => 'member', 'status' => 'active']);
$managerRoleSpec = new RoleHierarchySpecification($tenant, 'manager');
$this->assertTrue($managerRoleSpec->isSatisfiedBy($admin)); // Admin > Manager
$this->assertTrue($managerRoleSpec->isSatisfiedBy($manager)); // Manager = Manager
$this->assertFalse($managerRoleSpec->isSatisfiedBy($member)); // Member < Manager
}
/** @test */
public function confidential_document_access_respects_tenant_type()
{
$enterpriseTenant = Tenant::factory()->create(['type' => 'enterprise']);
$teamTenant = Tenant::factory()->create(['type' => 'team']);
$seniorMember = User::factory()->create();
$seniorMember->tenants()->attach($enterpriseTenant->id, ['role' => 'senior_member', 'status' => 'active']);
$seniorMember->tenants()->attach($teamTenant->id, ['role' => 'senior_member', 'status' => 'active']);
$confidentialDoc = Document::factory()->create(['is_confidential' => true]);
// Enterprise tenant: senior member can access
$enterpriseSpec = new ConfidentialDocumentAccessSpecification($seniorMember, $enterpriseTenant);
$this->assertTrue($enterpriseSpec->isSatisfiedBy($confidentialDoc));
// Team tenant: senior member cannot access
$teamSpec = new ConfidentialDocumentAccessSpecification($seniorMember, $teamTenant);
$this->assertFalse($teamSpec->isSatisfiedBy($confidentialDoc));
}
}
Key Learnings
This advanced example demonstrates:
✅ Complex Authorization Logic: Multi-layered permission systems
✅ Role Hierarchies: Sophisticated role-based access control
✅ Tenant-Aware Logic: Different rules for different tenant types
✅ Composite Specifications: Building complex authorization from simple rules
✅ Laravel Integration: Gates, policies, and middleware working together
✅ Performance Optimization: Query-level filtering with specifications
✅ Comprehensive Testing: Integration and unit testing of complex scenarios
Next Steps
With your multi-tenant authorization system:
- Add audit logging to track authorization decisions
- Implement permission caching for high-traffic applications
- Create admin interfaces for managing roles and permissions
- Add API authentication with the same specification-driven logic
Ready for the ultimate challenge? Tackle Financial Risk Assessment with external API integration.