# Role and Permission System

## Overview

SuiteX implements a two-tier role-based access control (RBAC) system that provides both global role permissions and granular record-type-specific permissions. This document describes the database schema, permission logic, and implementation patterns.

## Database Schema

### Tables Overview

The permission system uses three primary tables:

1. **`roles`** - Defines user roles with global permissions
2. **`role_permissions`** - Defines record-type-specific permission overrides
3. **`record_types`** - Catalog of all record types in the system

### Schema: `roles` Table

**Location:** Tenant database (`tenant_connection`)

**Purpose:** Store roles with global CRUD permissions that apply by default.

```sql
CREATE TABLE roles (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,           -- Role display name (e.g., "Project Manager")
    key VARCHAR(255) NULL,                 -- Optional unique identifier
    description TEXT NULL,                  -- Role description
    
    -- Global CRUD Permissions (fallback defaults)
    can_read BOOLEAN DEFAULT 1,            -- Can view records (default: yes)
    can_create BOOLEAN DEFAULT 1,          -- Can create records (default: yes)
    can_update BOOLEAN DEFAULT 1,          -- Can modify records (default: yes)
    can_delete BOOLEAN DEFAULT 0,          -- Can delete records (default: no)
    
    -- System Permissions
    can_manage_roles BOOLEAN DEFAULT 0,    -- Can create/edit roles
    can_manage_users BOOLEAN DEFAULT 0,    -- Can manage user accounts
    can_manage_ipass BOOLEAN DEFAULT 0,    -- Can manage iPaaS flows
    can_manage_adhoc BOOLEAN DEFAULT 0,    -- Can run ad-hoc queries
    can_manage_documents BOOLEAN DEFAULT 0,-- Can manage document library
    
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL
);
```

**Key Concepts:**
- **Global Permissions**: `can_read`, `can_create`, `can_update`, `can_delete` apply to ALL record types by default
- **Fallback Logic**: Used when no record-type-specific permission exists
- **System Permissions**: Control access to administrative features (not record-based)

**Example Roles:**

| Role | can_read | can_create | can_update | can_delete | Description |
|------|----------|------------|------------|------------|-------------|
| Administrator | 1 | 1 | 1 | 1 | Full access to all records |
| Project Manager | 1 | 1 | 1 | 0 | Can manage but not delete |
| Viewer | 1 | 0 | 0 | 0 | Read-only access |
| Custom | 0 | 0 | 0 | 0 | Permissions defined per record type |

### Schema: `role_permissions` Table

**Location:** Tenant database (`tenant_connection`)

**Purpose:** Store record-type-specific permission overrides that take precedence over role defaults.

```sql
CREATE TABLE role_permissions (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    role_id BIGINT UNSIGNED NOT NULL,             -- FK to roles.id
    record_type_scriptid VARCHAR(255) NOT NULL,   -- FK to record_types.scriptid
    
    -- Record-Type-Specific CRUD Permissions (overrides)
    can_read BOOLEAN NULL,                        -- NULL = use role default
    can_create BOOLEAN NULL,                      -- NULL = use role default
    can_update BOOLEAN NULL,                      -- NULL = use role default
    can_delete BOOLEAN NULL,                      -- NULL = use role default
    
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    
    UNIQUE KEY unique_role_record_type (role_id, record_type_scriptid),
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
```

**Key Concepts:**
- **Override Mechanism**: Non-null values override role's global permissions
- **NULL = Fallback**: NULL values mean "use the role's default permission"
- **Granular Control**: Allows per-record-type permissions (e.g., "can edit Projects but not Invoices")
- **Unique Constraint**: One permission entry per role+record_type combination

**Example Permission Overrides:**

| role_id | record_type_scriptid | can_read | can_create | can_update | can_delete | Effect |
|---------|---------------------|----------|------------|------------|------------|--------|
| 3 | project | 1 | 1 | 1 | NULL | Role 3 can manage projects (delete uses role default) |
| 3 | invoice | 1 | 0 | 0 | 0 | Role 3 can only view invoices (no editing) |
| 5 | project | NULL | NULL | NULL | NULL | Role 5 uses all defaults for projects |
| 7 | invoice | 0 | NULL | NULL | NULL | Role 7 cannot see invoices (create/update/delete use defaults) |

### Schema: `record_types` Table

**Location:** Tenant database (`tenant_connection`)

**Purpose:** Catalog of all record types available in the system.

```sql
CREATE TABLE record_types (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    scriptid VARCHAR(255) NOT NULL UNIQUE,    -- Unique identifier (e.g., 'project', 'invoice')
    model_name VARCHAR(255) NOT NULL,         -- Model class name (e.g., 'Project', 'Invoice')
    label VARCHAR(255) NOT NULL,              -- Display name (e.g., 'Projects')
    icon VARCHAR(255) NULL,                   -- UI icon reference
    nestable BOOLEAN DEFAULT 0,               -- Supports parent-child hierarchy
    -- ... other metadata fields
);
```

**Common Record Types:**

| scriptid | model_name | label | nestable |
|----------|-----------|-------|----------|
| project | Project | Projects | 1 |
| projecttask | ProjectTask | Project Tasks | 1 |
| subtask | Subtask | Subtasks | 0 |
| invoice | Invoice | Invoices | 0 |
| estimate | Estimate | Estimates | 0 |
| customer | Customer | Customers | 0 |

## Permission Logic Flow

### The hasPermissionForRecordType() Method

**Location:** `src/App/Models/User.php` (line ~277)

**Signature:**
```php
public function hasPermissionForRecordType(string $permission, string $recordType): bool
```

**Parameters:**
- `$permission` - One of: `'can_read'`, `'can_create'`, `'can_update'`, `'can_delete'`
- `$recordType` - Record type scriptid (e.g., `'project'`, `'invoice'`)

**Return:** `true` if user has permission, `false` otherwise

### Permission Resolution Algorithm

```
┌─────────────────────────────────────────────┐
│ User requests permission for record type   │
└─────────────────┬───────────────────────────┘
                  │
                  ▼
┌─────────────────────────────────────────────┐
│ Check: Does role_permissions have entry    │
│ for this role + record_type_scriptid?      │
└─────────────────┬───────────────────────────┘
                  │
        ┌─────────┴─────────┐
        │                   │
       YES                 NO
        │                   │
        ▼                   ▼
┌──────────────┐   ┌──────────────────────┐
│ Check value  │   │ Use role's global    │
│ of specific  │   │ permission as        │
│ permission   │   │ fallback             │
│ column       │   │                      │
└──────┬───────┘   └──────────┬───────────┘
       │                      │
       ▼                      │
  NULL? ────────NO───────┐    │
       │                 │    │
      YES                │    │
       │                 │    │
       │                 ▼    ▼
       │          ┌──────────────┐
       └─────────▶│ Return value │
                  └──────────────┘
```

**Pseudo-code:**

```php
function hasPermissionForRecordType($permission, $recordType) {
    $role = $this->currentRole;
    
    // Step 1: Look for record-type-specific permission
    $specific = $role->permissions()
        ->where('record_type_scriptid', $recordType)
        ->first();
    
    // Step 2: If specific permission exists and is non-null, use it
    if ($specific && $specific->$permission !== null) {
        return (bool) $specific->$permission;
    }
    
    // Step 3: Otherwise, fall back to role's global permission
    return (bool) $role->$permission;
}
```

### Caching Strategy

The method implements **two-tier caching** for performance:

**Tier 1: Request-Scope Cache (in-memory)**
- Stored in `$this->permissionCache` array
- Key format: `"tenant_{$tenantId}|role_{$roleId}|{$permission}|{$recordType}"`
- Lifetime: Single request
- Purpose: Avoid redundant checks within same request

**Tier 2: Cross-Request Cache (Redis/Memcached)**
- Key format: `"tenant_{$tenantId}_user_{$userId}_role_{$roleId}_perm_{$permission}_{$recordType}"`
- TTL: 600 seconds (10 minutes)
- Purpose: Reduce database queries across requests

**Cache Invalidation:**
- Automatic expiry after 10 minutes
- Manual flush required when role permissions change
- Tenant context included in key to prevent cross-tenant leakage

## Eloquent Relationships

### Role Model

**Location:** `src/Domain/Roles/Models/Role.php`

```php
class Role extends Model
{
    use UsesTenantConnection;
    
    protected $connection = 'tenant_connection';
    protected $guarded = [];
    
    // Relationships
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
    
    public function permissions()
    {
        return $this->hasMany(RolePermission::class);
    }
    
    public function dashboardBuilders()
    {
        return $this->belongsToMany(Dashboard::class, 'dashboard_builder_roles');
    }
    
    public function tableBuilders()
    {
        return $this->belongsToMany(TableBuilder::class, 'table_builder_roles');
    }
    
    public function timelineFilters()
    {
        return $this->belongsToMany(TimelineFilter::class, 'timeline_filter_roles');
    }
}
```

### RolePermission Model

**Location:** `src/Domain/RolePermissions/Models/RolePermission.php`

```php
class RolePermission extends Model
{
    use UsesTenantConnection;
    
    protected $connection = 'tenant_connection';
    protected $table = 'role_permissions';
    
    protected $fillable = [
        'role_id',
        'record_type_scriptid',
        'can_create',
        'can_read',
        'can_update',
        'can_delete',
    ];
    
    // Relationships
    public function role()
    {
        return $this->belongsTo(Role::class);
    }
    
    public function recordType()
    {
        return $this->belongsTo(RecordType::class, 'record_type_scriptid', 'scriptid');
    }
}
```

## Common Usage Patterns

### Pattern 1: Check Permission in Controller

```php
use App\Traits\AuthorizesRecordTypes;

class ProjectController extends Controller
{
    use AuthorizesRecordTypes;
    
    public function index()
    {
        // Check if user can read projects
        if ($response = $this->authorizeRecordTypeOrJson('can_read', 'project')) {
            return $response;  // 403 Unauthorized
        }
        
        // User has permission - proceed
        $projects = Project::all();
        return response()->json($projects);
    }
}
```

### Pattern 2: Check Permission in Blade

```php
// Controller
public function show($id)
{
    $project = Project::findOrFail($id);
    
    $canUpdate = $this->canUpdateRecordType('project');
    $canDelete = $this->canDeleteRecordType('project');
    
    return view('projects.show', compact('project', 'canUpdate', 'canDelete'));
}
```

```blade
{{-- Blade Template --}}
@if($canUpdate)
    <button wire:click="update">Save Changes</button>
@endif

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

### Pattern 3: Grant Specific Permission

```php
use Domain\Roles\Models\Role;
use Domain\RolePermissions\Models\RolePermission;

// Grant "Project Manager" role permission to manage invoices
$role = Role::where('title', 'Project Manager')->first();

RolePermission::create([
    'role_id' => $role->id,
    'record_type_scriptid' => 'invoice',
    'can_read' => true,
    'can_create' => true,
    'can_update' => true,
    'can_delete' => false,  // Still cannot delete invoices
]);
```

### Pattern 4: Revoke Specific Permission

```php
// Deny "Member" role access to sensitive record type
$role = Role::where('title', 'Member')->first();

RolePermission::create([
    'role_id' => $role->id,
    'record_type_scriptid' => 'financialreport',
    'can_read' => false,     // Explicit denial
    'can_create' => false,
    'can_update' => false,
    'can_delete' => false,
]);

// Even if role.can_read = 1, this specific permission blocks access
```

## Permission Scenarios

### Scenario 1: Default Admin Role

**Setup:**
```sql
-- Role with all global permissions enabled
INSERT INTO roles (title, can_read, can_create, can_update, can_delete)
VALUES ('Administrator', 1, 1, 1, 1);
```

**Result:**
- ✅ Can perform all operations on ALL record types
- ✅ No specific permissions needed in `role_permissions`
- ✅ Fallback logic grants universal access

### Scenario 2: Read-Only Role

**Setup:**
```sql
-- Role with only read permission
INSERT INTO roles (title, can_read, can_create, can_update, can_delete)
VALUES ('Viewer', 1, 0, 0, 0);
```

**Result:**
- ✅ Can view all record types
- ❌ Cannot create, update, or delete any records
- ⚠️ Can be overridden per record type if needed

### Scenario 3: Custom Role with Specific Overrides

**Setup:**
```sql
-- Role with no global permissions
INSERT INTO roles (title, can_read, can_create, can_update, can_delete)
VALUES ('Custom', 0, 0, 0, 0);

-- Grant specific permissions per record type
INSERT INTO role_permissions (role_id, record_type_scriptid, can_read, can_create, can_update, can_delete)
VALUES 
    (3, 'project', 1, 1, 1, 0),     -- Can manage projects (no delete)
    (3, 'projecttask', 1, 1, 1, 1), -- Full access to tasks
    (3, 'invoice', 1, 0, 0, 0);     -- Can only view invoices
```

**Result:**
- ✅ Projects: Read, create, update (no delete)
- ✅ Tasks: Full access
- ✅ Invoices: Read-only
- ❌ All other record types: No access (global permissions are all 0)

### Scenario 4: Mostly Open with Specific Restrictions

**Setup:**
```sql
-- Role with most permissions enabled
INSERT INTO roles (title, can_read, can_create, can_update, can_delete)
VALUES ('Standard User', 1, 1, 1, 0);

-- Block access to sensitive record types
INSERT INTO role_permissions (role_id, record_type_scriptid, can_read, can_create, can_update, can_delete)
VALUES 
    (4, 'financialreport', 0, 0, 0, 0),  -- No access to financial reports
    (4, 'payroll', 0, 0, 0, 0);          -- No access to payroll
```

**Result:**
- ✅ Most record types: Read, create, update (no delete) via fallback
- ❌ Financial reports: No access (explicit denial)
- ❌ Payroll: No access (explicit denial)

## Testing Permissions

### Unit Tests

**Test permission resolution logic:**

```php
describe('Permission Resolution', function () {
    beforeEach(function () {
        $this->user = User::factory()->create();
        $this->role = $this->user->currentRole;
    });
    
    it('uses global permission when no specific permission exists', function () {
        $this->role->update(['can_read' => true]);
        
        expect($this->user->hasPermissionForRecordType('can_read', 'project'))
            ->toBeTrue();
    });
    
    it('uses specific permission when it exists', function () {
        $this->role->update(['can_read' => false]); // Global: deny
        
        RolePermission::create([
            'role_id' => $this->role->id,
            'record_type_scriptid' => 'project',
            'can_read' => true,  // Specific: allow
        ]);
        
        expect($this->user->hasPermissionForRecordType('can_read', 'project'))
            ->toBeTrue();  // Specific permission overrides global
    });
    
    it('uses global permission when specific permission is NULL', function () {
        $this->role->update(['can_read' => true]);
        
        RolePermission::create([
            'role_id' => $this->role->id,
            'record_type_scriptid' => 'project',
            'can_read' => null,  // NULL = use fallback
        ]);
        
        expect($this->user->hasPermissionForRecordType('can_read', 'project'))
            ->toBeTrue();  // Falls back to role.can_read
    });
});
```

### Integration Tests

**Test end-to-end authorization:**

```php
describe('Authorization Integration', function () {
    it('blocks access when permission is denied', function () {
        $user = User::factory()->create();
        $user->currentRole->update(['can_read' => false]);
        
        actingAs($user)
            ->getJson('/api/projects')
            ->assertStatus(403)
            ->assertJson(['error' => 'Unauthorized']);
    });
    
    it('allows access when permission is granted', function () {
        $user = User::factory()->create();
        $user->currentRole->update(['can_read' => true]);
        
        actingAs($user)
            ->getJson('/api/projects')
            ->assertStatus(200);
    });
});
```

## Data Model Diagram

```
┌──────────────────────────────────────────────┐
│                   User                        │
│ ──────────────────────────────────────────── │
│ - id                                          │
│ - current_role_id  ────────┐                 │
└──────────────────────────────│────────────────┘
                               │
                               │ belongs to
                               │
                               ▼
┌──────────────────────────────────────────────┐
│                   Role                        │
│ ──────────────────────────────────────────── │
│ - id                                          │
│ - title                                       │
│ - can_read       (GLOBAL FALLBACK)           │
│ - can_create     (GLOBAL FALLBACK)           │
│ - can_update     (GLOBAL FALLBACK)           │
│ - can_delete     (GLOBAL FALLBACK)           │
│ - can_manage_*   (SYSTEM PERMISSIONS)        │
└─────────┬────────────────────────────────────┘
          │
          │ has many
          │
          ▼
┌──────────────────────────────────────────────┐
│            RolePermission                     │
│ ──────────────────────────────────────────── │
│ - id                                          │
│ - role_id                                     │
│ - record_type_scriptid  ──────┐              │
│ - can_read       (OVERRIDE)   │              │
│ - can_create     (OVERRIDE)   │              │
│ - can_update     (OVERRIDE)   │              │
│ - can_delete     (OVERRIDE)   │              │
└────────────────────────────────│──────────────┘
                                 │
                                 │ belongs to
                                 │
                                 ▼
┌──────────────────────────────────────────────┐
│               RecordType                      │
│ ──────────────────────────────────────────── │
│ - id                                          │
│ - scriptid      (e.g., 'project')            │
│ - model_name    (e.g., 'Project')            │
│ - label         (e.g., 'Projects')           │
└──────────────────────────────────────────────┘
```

## Related Documentation

- [Authorization Pattern](./user-auth-checks.md) - How to implement authorization checks
- [Multi-Tenancy Security](./multi-tenancy.md) - Tenant isolation patterns

## Changelog

| Date | Author | Change |
|------|--------|--------|
| 2026-02-18 | System | Initial documentation of role and permission system |
