# Authorization Pattern for Controllers

## Overview

SuiteX uses the `AuthorizesRecordTypes` trait to provide consistent, secure authorization checks across all controllers. This document describes how to implement authorization using the trait and understanding the underlying permission system.

## Authorization Architecture

### Two-Tier Permission System

SuiteX implements a layered permission system with automatic fallback:

1. **Record-Type-Specific Permissions** (Primary)
   - Stored in `role_permissions` table
   - Granular control per record type (e.g., "can view Projects but not Tasks")
   - Checked first by `User::hasPermissionForRecordType()`

2. **Role-Level Global Permissions** (Fallback)
   - Stored in `roles` table (`can_read`, `can_create`, `can_update`, `can_delete`)
   - Applies when no record-type-specific permission exists
   - Ensures backward compatibility

### Permission Hierarchy

```
User requests access to Record Type "Project"
    ↓
Check: Does role_permissions have entry for this role + "Project"?
    ↓
YES → Use that permission (specific override)
NO  → Fall back to roles.can_read (global default)
```

## Using the AuthorizesRecordTypes Trait

### Setup

Add the trait to any controller that needs authorization:

```php
use App\Traits\AuthorizesRecordTypes;

class TableController extends Controller
{
    use AuthorizesRecordTypes;
    
    // ... controller methods
}
```

### Authorization Methods

The trait provides two types of methods:

#### **Blocking Methods** (abort/throw on failure)

Use when you want execution to stop if authorization fails:

| Method | Use Case | Response Type |
|--------|----------|---------------|
| `authorizeRecordTypeOrJson()` | API endpoints | Returns `JsonResponse` on failure |
| `authorizeRecordTypeOrAbort()` | Web routes | Calls `abort(403)` on failure |
| `authorizeRecordTypeOrFail()` | Auto-detect | Detects JSON vs. web automatically |

#### **Non-Blocking Methods** (return boolean)

Use for conditional UI rendering or pre-flight checks:

| Method | Purpose |
|--------|---------|
| `canReadRecordType($recordType)` | Check read permission |
| `canCreateRecordType($recordType)` | Check create permission |
| `canUpdateRecordType($recordType)` | Check update permission |
| `canDeleteRecordType($recordType)` | Check delete permission |
| `hasPermissionFor($permission, $recordType)` | Generic permission check |

### Critical Placement Rule

**Authorization checks must be placed:**
- ✅ **AFTER** request validation
- ✅ **AFTER** fetching required context (record types, parent records)
- ✅ **BEFORE** any database queries or business logic
- ✅ **BEFORE** any data access or transformation

This ensures fail-fast security with no data leakage.

## Common Implementation Patterns

### Pattern 1: API Endpoints (Explicit JSON Response)

**Recommended for API controllers** - clearest intent, returns early on failure.

```php
use App\Traits\AuthorizesRecordTypes;

class TableController extends Controller
{
    use AuthorizesRecordTypes;

    public function getRecords(Request $request, string $recordType): JsonResponse
    {
        $recordTypeModel = $this->getRecordType($recordType);
        
        // Authorization check - returns JsonResponse if unauthorized
        if ($response = $this->authorizeRecordTypeOrJson('can_read', $recordType)) {
            return $response;
        }
        
        // Business logic only runs if authorized
        $records = GetModel::run($recordTypeModel->model_name);
        return response()->json($records);
    }

    public function updateField(Request $request, string $recordType): JsonResponse
    {
        $validated = $request->validate([...]);
        $recordTypeModel = $this->getRecordType($recordType);
        
        // Check update permission
        if ($response = $this->authorizeRecordTypeOrJson('can_update', $recordType)) {
            return $response;
        }
        
        // Update logic
        $record = $modelClass::findOrFail($validated['record_id']);
        $record->update($validated);
        return response()->json(['success' => true]);
    }
}
```

### Pattern 2: Web Controllers (Abort on Failure)

**For traditional web routes** - uses Laravel's `abort()` to trigger 403 error pages.

```php
use App\Traits\AuthorizesRecordTypes;

class RecordController extends Controller
{
    use AuthorizesRecordTypes;

    public function show(string $recordType, int $id)
    {
        $recordTypeModel = $this->getRecordType($recordType);
        
        // Aborts with 403 if unauthorized
        $this->authorizeRecordTypeOrAbort('can_read', $recordType);
        
        // Business logic
        $record = GetModel($recordTypeModel->model_name)::findOrFail($id);
        return view('records.show', compact('record'));
    }

    public function update(Request $request, string $recordType, int $id)
    {
        $validated = $request->validate([...]);
        
        // Abort if no update permission
        $this->authorizeRecordTypeOrAbort('can_update', $recordType);
        
        // Update logic
        $record = GetModel($recordType)::findOrFail($id);
        $record->update($validated);
        return redirect()->route('records.show', [$recordType, $id]);
    }
}
```

### Pattern 3: Conditional UI Rendering

**For showing/hiding UI elements** - non-blocking checks that return booleans.

```php
use App\Traits\AuthorizesRecordTypes;

class ProjectController extends Controller
{
    use AuthorizesRecordTypes;

    public function show(int $id)
    {
        $project = Project::findOrFail($id);
        
        // Non-blocking permission checks for UI
        $canUpdate = $this->canUpdateRecordType('project');
        $canDelete = $this->canDeleteRecordType('project');
        
        return view('projects.show', [
            'project' => $project,
            'canUpdate' => $canUpdate,
            'canDelete' => $canDelete,
        ]);
    }
}
```

**In Blade templates:**
```blade
@if($canUpdate)
    <button wire:click="update">Save Changes</button>
@else
    <p class="text-gray-500">You don't have permission to edit this project.</p>
@endif

@if($canDelete)
    <button wire:click="delete" class="btn-danger">Delete Project</button>
@endif
```

### Pattern 4: Related Resource Authorization

**When accessing comments, attachments, or child records:**

```php
public function getComments(Request $request): JsonResponse
{
    $validated = $request->validate([
        'model_name' => 'required|string',
        'record_id' => 'required|integer',
    ]);
    
    // Fetch parent record
    $model = app("App\\Models\\{$validated['model_name']}")->findOrFail($validated['record_id']);
    
    // Derive record type from parent
    $recordType = $this->getRecordTypeFromModel($model);
    
    // Authorize against PARENT record type (not 'comment')
    if ($response = $this->authorizeRecordTypeOrJson('can_read', $recordType)) {
        return $response;
    }
    
    // Fetch comments
    $comments = $model->comments()->get();
    return response()->json($comments);
}
```

**Rule:** When accessing related resources, authorize against the **parent resource's permissions**.

## Row-Level Authorization (Ownership Checks)

**Important:** The trait handles **record-type permissions only**. For ownership checks (e.g., "users can only edit their own comments"), use inline checks AFTER fetching the model:

```php
public function updateComment(Request $request, int $id): JsonResponse
{
    // 1. Record-type permission check
    if ($response = $this->authorizeRecordTypeOrJson('can_update', 'comment')) {
        return $response;
    }
    
    // 2. Fetch the record
    $comment = Comment::findOrFail($id);
    
    // 3. Ownership check (separate from permissions)
    if ($comment->creator_id != auth()->id()) {
        return response()->json([
            'success' => false,
            'message' => 'You can only edit your own comments.'
        ], 403);
    }
    
    // 4. Business logic
    $comment->update($request->validated());
    return response()->json(['success' => true]);
}
```

**Why separate?** Record-type permissions and ownership are different concerns with different semantics and error messages.

## Audit Logging

The trait includes optional audit logging for failed authorization attempts. Enable in `config/auth.php`:

```php
'log_authorization_failures' => env('LOG_AUTH_FAILURES', false),
```

When enabled, failed authorizations are logged with:
- User ID and role
- Permission and record type requested
- Route, IP, and user agent
- Tenant context (if multi-tenant)

Use for security monitoring, compliance audits, and debugging permission issues.

## Testing Authorization

### Test Matrix

| Role Setup | Specific Permission | Expected Result |
|------------|-------------------|-----------------|
| `can_read=true` | None | ✅ Access granted (fallback) |
| `can_read=false` | None | ❌ 403 Forbidden |
| `can_read=false` | `can_read=true` | ✅ Access granted (override) |
| `can_read=true` | `can_read=false` | ❌ 403 Forbidden (override) |

### Example Tests

```php
describe('TableController Authorization', function () {
    it('denies access when user lacks permission', function () {
        $user = User::factory()->create();
        $role = $user->role;
        $role->update(['can_read' => false]);
        $role->permissions()->delete();
        
        actingAs($user)
            ->getJson(route('api.table.records', ['recordType' => 'project']))
            ->assertStatus(403)
            ->assertJson(['error' => 'Unauthorized']);
    });
    
    it('allows access with global permission', function () {
        $user = User::factory()->create();
        $user->role->update(['can_read' => true]);
        
        actingAs($user)
            ->getJson(route('api.table.records', ['recordType' => 'project']))
            ->assertStatus(200);
    });
    
    it('allows access with record-type-specific permission', function () {
        $user = User::factory()->create();
        $role = $user->role;
        $role->update(['can_read' => false]); // Global disabled
        
        // Grant specific permission
        RolePermission::create([
            'role_id' => $role->id,
            'record_type_id' => RecordType::where('recordType', 'project')->first()->id,
            'can_read' => true,
        ]);
        
        actingAs($user)
            ->getJson(route('api.table.records', ['recordType' => 'project']))
            ->assertStatus(200);
    });
});
```

## Code Review Checklist

When reviewing authorization implementation:

- [ ] Controller uses `AuthorizesRecordTypes` trait
- [ ] Authorization check is placed AFTER validation
- [ ] Authorization check is placed BEFORE business logic
- [ ] Correct method used (`authorizeRecordTypeOrJson` for API, `authorizeRecordTypeOrAbort` for web)
- [ ] Correct permission type (`can_read`, `can_create`, `can_update`, `can_delete`)
- [ ] Correct record type identified (not hardcoded unless appropriate)
- [ ] No data queries or transformations before authorization
- [ ] Tests cover denied and allowed scenarios
- [ ] Tests verify fallback behavior

## Common Mistakes

### ❌ Checking authorization after data access
```php
// WRONG - data fetched before check
$records = GetModel::run($recordType);
if (!$this->canReadRecordType($recordType)) {
    return response()->json(['error' => 'Unauthorized'], 403);
}
```

### ❌ Wrong permission type
```php
// WRONG - using can_read for delete operation
if ($response = $this->authorizeRecordTypeOrJson('can_read', $recordType)) {
    return $response;
}
$model->delete();
```

### ❌ Using can* for blocking authorization
```php
// WRONG - can* methods don't abort, need explicit check
if (!$this->canUpdateRecordType($recordType)) {
    // Execution continues! Need return statement
}
$model->update($data);
```

**Correct:**
```php
// Use authorize* methods which abort automatically
if ($response = $this->authorizeRecordTypeOrJson('can_update', $recordType)) {
    return $response;
}
$model->update($data);
```

## Related Documentation

- [Multi-Tenancy Security](./multi-tenancy.md) - Tenant isolation patterns
- [Role and Permission System](./roles-permissions.md) - Permission model schema

## Changelog

| Date | Author | Change |
|------|--------|--------|
| 2026-02-18 | System | Integrated AuthorizesRecordTypes trait pattern |
| 2026-02-18 | System | Initial documentation based on authorization implementation plan |
