# Multi-Tenancy Security

## Overview

SuiteX implements **database-level tenancy** (separate databases per tenant) to ensure complete data isolation. This document describes the security mechanisms that prevent cross-tenant data leakage.

## Tenancy Architecture

### Database-Level Isolation

SuiteX uses **separate databases** for each tenant, NOT row-level tenancy with `tenant_id` columns.

**Architecture Benefits:**
- ✅ **Complete data isolation** - no possibility of cross-tenant queries
- ✅ **Simplified queries** - no need to scope every query with `tenant_id`
- ✅ **Performance** - database-level indexes and query optimization
- ✅ **Backup & restore** - tenant databases are independent units

**Architecture Trade-offs:**
- ⚠️ **Connection management** - must use correct connection for tenant models
- ⚠️ **Schema migrations** - must run against all tenant databases
- ⚠️ **Cross-tenant queries** - not possible (by design for security)

### Connection Architecture

SuiteX uses two database connections:

```php
// config/database.php

'connections' => [
    // Core/System Connection (default)
    'mysql' => [
        'driver' => 'mysql',
        'database' => env('DB_DATABASE', 'suitex_core'),
        // Stores: accounts, instances, users (non-tenant data)
    ],
    
    // Tenant Connection (dynamic)
    'tenant_connection' => [
        'driver' => 'mysql',
        'database' => env('DB_DATABASE', 'tenant_12345'), // Set at runtime
        // Stores: projects, tasks, invoices (tenant-specific data)
    ],
]
```

**Connection Usage:**
- **`mysql` (default)**: Core system data (accounts, instances, jobs, queues)
- **`tenant_connection`**: All tenant-specific business data

## The UsesTenantConnection Trait

### Purpose

The `UsesTenantConnection` trait forces Eloquent models to use the `tenant_connection` database connection, preventing accidental queries against the wrong database.

### Implementation

**Trait Location:** `src/App/Traits/UsesTenantConnection.php`

```php
trait UsesTenantConnection
{
    /**
     * Get the database connection for the model.
     */
    public function getConnectionName()
    {
        return 'tenant_connection';
    }

    /**
     * Set up the tenant connection.
     */
    protected function setupTenantConnection()
    {
        if (property_exists($this, 'tenantService')) {
            $this->tenantService->setTenantConfiguration(
                config('database.connections.tenant_connection.database'),
                config('app.url')
            );
        }
    }
}
```

### Usage Pattern

**All tenant models MUST use this trait:**

```php
namespace Domain\Projects\Models;

use App\Traits\UsesTenantConnection;
use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    use UsesTenantConnection;
    
    protected $connection = 'tenant_connection';  // Explicit declaration
    protected $guarded = ['field'];               // Dynamic schema protection
    
    // ... model logic
}
```

**Why both trait AND `$connection` property?**
- Trait provides the `getConnectionName()` method (runtime enforcement)
- Property provides static declaration (IDE/PHPStan support)
- Both reinforce security through redundancy

### Directory Convention

**All models in `src/Domain/` MUST use `UsesTenantConnection`:**

```
src/Domain/
├── Projects/Models/Project.php          ✅ Uses trait
├── ProjectTasks/Models/ProjectTask.php  ✅ Uses trait
├── Invoices/Models/Invoice.php          ✅ Uses trait
└── Comments/Models/Comment.php          ✅ Uses trait
```

**Models in `src/App/Models/` are system-level** (use default `mysql` connection):

```
src/App/Models/
├── User.php            ❌ NO trait (multi-tenant system user)
├── Account.php         ❌ NO trait (system-level)
└── Instance.php        ❌ NO trait (tenant metadata)
```

## Dynamic Model Resolution Security

### The Problem

Controllers often need to resolve models dynamically:

```php
// DANGEROUS: No validation that model is tenant-scoped
$modelClass = "App\\Models\\{$modelName}";
$record = $modelClass::find($id);  // Could query wrong database!
```

### The Solution: GetTenantModel() Helper

**Helper Location:** `src/App/Helpers/helpers.php`

```php
/**
 * Get a tenant-scoped model class with security validation
 * 
 * SECURITY: Ensures dynamically resolved models use UsesTenantConnection
 * to prevent IDOR vulnerabilities and cross-tenant data leakage.
 */
function GetTenantModel(string $modelName): string
{
    // 1. Resolve model class name
    $modelClass = ResolveModelClass($modelName);
    
    // 2. Validate it uses UsesTenantConnection trait
    ValidateTenantModel($modelClass, ['model_name' => $modelName]);
    
    // 3. Return validated class name
    return $modelClass;
}
```

### Usage in Controllers

**Correct Pattern:**

```php
use function GetTenantModel;

class TableController extends Controller
{
    public function getRecords(Request $request, string $recordType)
    {
        // Safe: Validates tenant connection before use
        $modelClass = GetTenantModel($recordType);
        $records = $modelClass::where(...)->get();
        
        return response()->json($records);
    }
}
```

**Incorrect Pattern (NEVER DO THIS):**

```php
// ❌ DANGEROUS: No validation
$modelClass = "App\\Models\\{$recordType}";
$record = $modelClass::find($id);  // Could leak cross-tenant data!
```

### Security Validation

The `ValidateTenantModel()` helper checks:

1. **Model class exists**
2. **Model uses `UsesTenantConnection` trait**
3. **Model is in `src/Domain/` namespace** (convention check)

**Failure Mode:**

```php
ValidateTenantModel('App\Models\User');
// Throws: HTTP 500
// "Security Risk: Model App\Models\User is not tenant-scoped"
```

This fail-closed approach ensures developers cannot accidentally bypass tenant isolation.

## IDOR Prevention

### What is IDOR?

**Insecure Direct Object Reference (IDOR)** occurs when:
- User A can access User B's data by guessing/manipulating record IDs
- Example: `/api/projects/123` returns data even if project belongs to different tenant

### How SuiteX Prevents IDOR

**Multi-Layer Protection:**

1. **Database-Level Isolation**
   - Each tenant has separate database
   - Query against `tenant_connection` ONLY sees that tenant's data
   - Cross-tenant access is physically impossible

2. **Trait Enforcement**
   - All tenant models use `UsesTenantConnection`
   - Models automatically connect to correct tenant database
   - No manual `where('tenant_id')` clauses needed

3. **Dynamic Resolution Validation**
   - `GetTenantModel()` validates trait usage
   - Prevents accidental use of non-tenant models
   - Fails loudly with HTTP 500 if validation fails

4. **Connection Middleware** (implicit)
   - Tenant connection is set based on authenticated user's instance
   - All queries within request scope use correct tenant database

### Example: IDOR Attack Fails

**Attack Scenario:**
```
Tenant A User tries to access Tenant B's project by manipulating URL:
GET /api/projects/999  (where 999 belongs to Tenant B)
```

**SuiteX Protection:**
```php
// Controller code
$modelClass = GetTenantModel('Project');  // ✅ Validated
$project = $modelClass::findOrFail(999);  // ✅ Connected to Tenant A's database

// Result: 404 Not Found (project 999 doesn't exist in Tenant A's database)
// Tenant B's data is in completely separate database - unreachable
```

**Why this is secure:**
- Query runs against `tenant_a_database`, which has no record 999
- Tenant B's database (`tenant_b_database`) is never queried
- No `tenant_id` to leak or bypass - physical database separation

## Common Patterns and Edge Cases

### Pattern 1: Standard Tenant Model

```php
use App\Traits\UsesTenantConnection;

class Invoice extends Model
{
    use UsesTenantConnection;
    
    protected $connection = 'tenant_connection';
    protected $guarded = ['field'];  // Dynamic schema
}
```

### Pattern 2: Model with Relationships

```php
class Project extends Model
{
    use UsesTenantConnection;
    
    protected $connection = 'tenant_connection';
    
    public function tasks()
    {
        // Relationships automatically use same connection
        return $this->hasMany(ProjectTask::class);
    }
}
```

**Important:** Related models (`ProjectTask`) MUST also use `UsesTenantConnection`.

### Pattern 3: Dynamic Model in Loop

```php
public function processRecords(array $recordTypes)
{
    foreach ($recordTypes as $recordType) {
        // Validate EACH dynamic model
        $modelClass = GetTenantModel($recordType);
        
        $records = $modelClass::where('status', 'pending')->get();
        // Process records...
    }
}
```

### Pattern 4: Cross-Tenant Queries (NEVER DO THIS)

```php
// ❌ WRONG: Trying to query across tenants
$allProjects = Project::on('mysql')->get();  // Bypasses tenant connection!

// ❌ WRONG: Manual connection switching
DB::connection('tenant_b')->table('projects')->get();  // Accesses wrong tenant!
```

**Why forbidden:**
- Violates tenant isolation
- Opens IDOR vulnerabilities
- Breaks compliance requirements

**Correct approach:**
If you need cross-tenant operations, use system-level tables (on `mysql` connection) that store references, not business data.

## Testing Tenant Isolation

### Unit Tests

**Test that models use correct connection:**

```php
describe('Project Model', function () {
    it('uses tenant connection', function () {
        $project = new Project();
        expect($project->getConnectionName())->toBe('tenant_connection');
    });
    
    it('uses UsesTenantConnection trait', function () {
        $traits = class_uses_recursive(Project::class);
        expect($traits)->toContain(UsesTenantConnection::class);
    });
});
```

### Integration Tests

**Test cross-tenant isolation:**

```php
describe('Tenant Isolation', function () {
    it('cannot access other tenant data', function () {
        // Setup: Create project in Tenant A
        switchTenant('tenant_a');
        $project = Project::create(['name' => 'Secret Project']);
        $projectId = $project->id;
        
        // Switch to Tenant B
        switchTenant('tenant_b');
        
        // Attempt to access Tenant A's project
        $result = Project::find($projectId);
        
        // Assert: Returns null (not found in Tenant B's database)
        expect($result)->toBeNull();
    });
});
```

## Code Review Checklist

When reviewing code for tenant security:

- [ ] All models in `src/Domain/` use `UsesTenantConnection` trait
- [ ] All models explicitly declare `protected $connection = 'tenant_connection'`
- [ ] Dynamic model resolution uses `GetTenantModel()` helper
- [ ] No manual connection switching (`->on()`, `DB::connection()`)
- [ ] No `tenant_id` columns in tables (we use database-level isolation)
- [ ] No cross-tenant queries
- [ ] Tests verify tenant isolation for new models

## Common Mistakes

### ❌ Mistake 1: Forgetting the Trait

```php
// WRONG - Missing trait
class Invoice extends Model
{
    protected $connection = 'tenant_connection';  // Not enough!
}
```

**Why it's wrong:** Property alone doesn't provide runtime enforcement.

**Correct:**
```php
use App\Traits\UsesTenantConnection;

class Invoice extends Model
{
    use UsesTenantConnection;
    protected $connection = 'tenant_connection';
}
```

### ❌ Mistake 2: Dynamic Model Without Validation

```php
// WRONG - No security validation
$modelClass = "Domain\\{$domain}\\Models\\{$modelName}";
$record = $modelClass::find($id);
```

**Correct:**
```php
$modelClass = GetTenantModel($modelName);
$record = $modelClass::find($id);
```

### ❌ Mistake 3: Checking for tenant_id Column

```php
// WRONG - We don't use row-level tenancy
$projects = Project::where('tenant_id', $tenantId)->get();
```

**Why it's wrong:** We use database-level tenancy, not `tenant_id` columns.

**Correct:**
```php
// No tenant_id needed - connection handles isolation
$projects = Project::where('status', 'active')->get();
```

## Migration Strategy

### Tenant Migrations

**Location:** `database/migrations/tenants/`

**Run against all tenant databases:**

```bash
# Custom command that iterates tenants
php artisan tenants:migrate
```

**Migration Pattern:**

```php
use Illuminate\Database\Migrations\Migration;

class CreateProjectsTable extends Migration
{
    // Runs against each tenant's database via tenant_connection
    public function up()
    {
        Schema::connection('tenant_connection')->create('projects', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            // NO tenant_id column needed!
            $table->timestamps();
        });
    }
}
```

### System Migrations

**Location:** `database/migrations/`

**Run against core database:**

```bash
php artisan migrate  # Uses default 'mysql' connection
```

## Related Documentation

- [Authorization Pattern](./user-auth-checks.md) - Record-type permissions
- [Role and Permission System](./roles-permissions.md) - Permission model schema

## Changelog

| Date | Author | Change |
|------|--------|--------|
| 2026-02-18 | System | Initial documentation of multi-tenancy architecture |
