Sindbad~EG File Manager

Current Path : /home/copmadinaarea/thecopmadinaarea.org/portal/docs/
Upload File :
Current File : /home/copmadinaarea/thecopmadinaarea.org/portal/docs/ACCESS_CONTROL_IMPLEMENTATION.md

# System-Wide Access Control Implementation Guide

## Overview
This document outlines the comprehensive access control system implemented across all modules to restrict data visibility based on user roles (Assembly Admin, District Admin, Area Admin, and Superuser).

## Access Level Hierarchy

```
Superuser (Top Level - See Everything)
    ↓
Area Admin (See all districts and assemblies in their area)
    ↓
District Admin (See all assemblies in their district)
    ↓
Assembly Admin (See only their specific assembly)
```

---

## Helper Functions Added to `config/config.php`

### 1. **Core Access Level Functions**

```php
// Check user roles
isSuperuser()         // Returns true if user is superuser
isAreaAdmin()         // Returns true if user is area admin or higher
isDistrictAdmin()     // Returns true if user is district admin or higher  
isAssemblyAdmin()     // Returns true if user is assembly admin or higher

// Get user identifiers
getUserAccessLevel()  // Returns: 'superuser', 'area', 'district', 'assembly'
getUserAreaId()       // Returns user's area_id
getUserDistrictId()   // Returns user's district_id
getUserAssemblyId()   // Returns user's assembly_id
```

### 2. **SQL Filter Functions**

#### **applyAccessLevelFilter($tableAlias, $options)**
Returns both WHERE clause and parameters for SQL queries.

**Parameters:**
- `$tableAlias`: Table alias (e.g., 'm' for members)
- `$options`: Array with keys:
  - `area_column`: Column name for area_id (default: 'area_id')
  - `district_column`: Column name for district_id (default: 'district_id')
  - `assembly_column`: Column name for assembly_id (default: 'assembly_id')
  - `bypass_superuser`: If true, superusers also get filtered (default: false)

**Returns:** `['where' => string, 'params' => array]`

**Example:**
```php
$filter = applyAccessLevelFilter('m');
$query = "SELECT * FROM members m WHERE 1=1" . $filter['where'];
$stmt = $db->prepare($query);
$stmt->execute($filter['params']);
```

#### **getAccessLevelWhere($tableAlias, $options)**
Quick helper that returns just the WHERE clause string.

**Example:**
```php
$query = "SELECT * FROM members m WHERE 1=1" . getAccessLevelWhere('m');
```

#### **getAccessLevelParams($tableAlias, $options)**
Quick helper that returns just the params array.

**Example:**
```php
$stmt->execute(getAccessLevelParams('m'));
```

### 3. **Record Access Validation**

#### **canAccessRecord($recordAreaId, $recordDistrictId, $recordAssemblyId)**
Checks if current user can view a specific record based on its location.

**Example:**
```php
if (!canAccessRecord($member['area_id'], $member['district_id'], $member['assembly_id'])) {
    die('Access denied');
}
```

### 4. **UI Helper Functions**

#### **getUserAccessScope()**
Returns human-readable description of user's access scope.

**Returns:** 
- "All Areas" (superuser)
- "Madina District" (district admin)
- "Central Assembly" (assembly admin)

#### **getUserAccessBadge()**
Returns HTML badge for displaying user's access level.

**Example Output:**
```html
<span class="px-3 py-1 rounded-full bg-yellow-100 text-yellow-800">
    <i class="fas fa-church mr-1"></i>Central Assembly
</span>
```

---

## Important: Events Table Structure

⚠️ **The `events` table uses a different structure:**
- Uses `location_type` (enum: 'area', 'district', 'assembly')
- Uses `location_id` (stores the ID of the area/district/assembly)
- Uses `start_date` and `end_date` (not `event_date`)

This means events require **custom filtering logic** instead of the standard helper functions.

**Example for Events:**
```php
$eventQuery = "SELECT * FROM events e WHERE e.is_active = 1";
$eventParams = [];

if (!isSuperuser()) {
    $accessLevel = getUserAccessLevel();
    if ($accessLevel === 'assembly') {
        $eventQuery .= " AND e.location_type = 'assembly' AND e.location_id = :location_id";
        $eventParams['location_id'] = getUserAssemblyId();
    } elseif ($accessLevel === 'district') {
        $eventQuery .= " AND e.location_type IN ('district', 'assembly')";
        // Add logic to check district or assemblies in that district
    }
}
```

See `dashboard.php` lines 19-43 for the complete events filtering implementation.

---

## Implementation Examples

### Example 1: Basic Member Query

**Before:**
```php
$query = "SELECT * FROM members WHERE is_active = 1";
$stmt = $db->prepare($query);
$stmt->execute();
```

**After:**
```php
$filter = applyAccessLevelFilter('m');
$query = "SELECT * FROM members m WHERE m.is_active = 1" . $filter['where'];
$stmt = $db->prepare($query);
$stmt->execute($filter['params']);
```

### Example 2: Count Query

**Before:**
```php
$stmt = $db->query("SELECT COUNT(*) as count FROM events");
```

**After:**
```php
$query = "SELECT COUNT(*) as count FROM events e WHERE 1=1" . getAccessLevelWhere('e');
$stmt = $db->prepare($query);
$stmt->execute(getAccessLevelParams('e'));
```

### Example 3: Complex Join Query

```php
$filter = applyAccessLevelFilter('m');

$query = "SELECT m.*, d.district_name, a.assembly_name, ar.area_name
          FROM members m
          LEFT JOIN districts d ON m.district_id = d.id
          LEFT JOIN assemblies a ON m.assembly_id = a.id
          LEFT JOIN areas ar ON m.area_id = ar.id
          WHERE m.is_active = 1" . $filter['where'] . "
          ORDER BY m.first_name";

$stmt = $db->prepare($query);
$stmt->execute($filter['params']);
```

### Example 4: With Additional Filters

```php
$filter = applyAccessLevelFilter('m');
$params = $filter['params'];

$query = "SELECT * FROM members m WHERE m.is_active = 1" . $filter['where'];

// Add search filter
if (!empty($searchTerm)) {
    $query .= " AND (m.first_name LIKE :search OR m.last_name LIKE :search)";
    $params['search'] = "%$searchTerm%";
}

$stmt = $db->prepare($query);
$stmt->execute($params);
```

### Example 5: Different Column Names

```php
// If table uses different column names
$filter = applyAccessLevelFilter('e', [
    'area_column' => 'event_area_id',
    'district_column' => 'event_district_id',
    'assembly_column' => 'event_assembly_id'
]);

$query = "SELECT * FROM events e WHERE 1=1" . $filter['where'];
```

---

## Files Already Updated

### ✅ Core Files
1. **config/config.php** - All helper functions added
2. **dashboard.php** - Full access control applied
3. **classes/Auth.php** - Session variables set on login

### ✅ Module Pages Already Using Access Control
1. **modules/membership/index.php**
2. **modules/membership/cards.php**
3. **modules/ministries/index.php**
4. **modules/member-codes/index.php**
5. **modules/programs/realtime-attendance.php**
6. **modules/attendance/live-qr.php**
7. **modules/member-accounts/index.php**

---

## Modules That Need Updating

Apply the access control filters to these modules:

### 1. Events Module
- `modules/events/index.php`
- `modules/events/view.php`
- `modules/events/attendance.php`

### 2. Programs Module  
- `modules/programs/index.php`
- `modules/programs/view.php`

### 3. Reports Module
- All report pages should filter data by access level

### 4. Communications Module
- `modules/communications/messages.php`
- `modules/communications/sms.php`

### 5. Other Modules
- Any module that displays member-related data
- Any module with area/district/assembly associations

---

## Quick Migration Template

```php
// OLD CODE
$stmt = $db->query("SELECT * FROM table_name");
$results = $stmt->fetchAll();

// NEW CODE WITH ACCESS CONTROL
$filter = applyAccessLevelFilter('t'); // 't' is table alias
$query = "SELECT * FROM table_name t WHERE 1=1" . $filter['where'];
$stmt = $db->prepare($query);
$stmt->execute($filter['params']);
$results = $stmt->fetchAll();
```

---

## Testing Access Control

### Test Accounts to Create

1. **Superuser** - Should see all data
   - access_level: 'superuser'
   - is_superuser: 1

2. **Area Admin** - Should see all districts/assemblies in area
   - access_level: 'area'
   - area_id: 1

3. **District Admin** - Should see all assemblies in district
   - access_level: 'district'
   - district_id: 1

4. **Assembly Admin** - Should see only their assembly
   - access_level: 'assembly'
   - assembly_id: 1

### Verification Checklist

For each user type, verify:
- [ ] Dashboard shows correct statistics
- [ ] Member list shows only accessible members
- [ ] Events list shows only accessible events
- [ ] Programs list shows only accessible programs
- [ ] Reports generate data for their scope only
- [ ] Cannot access records outside their scope
- [ ] Access badge displays correctly on dashboard

---

## Security Considerations

1. **Always use prepared statements** with the filter parameters
2. **Never bypass access control** unless explicitly required
3. **Validate record access** before showing detailed views
4. **Log access control violations** for security auditing
5. **Test thoroughly** with different user levels

---

## Display Access Badge on Pages

Add this to any page header to show user's access scope:

```php
<div class="mb-6">
    <?php echo getUserAccessBadge(); ?>
</div>
```

---

## Common Pitfalls to Avoid

1. **Forgetting to use table aliases**
   ```php
   // WRONG
   $filter = applyAccessLevelFilter(); // No alias
   
   // RIGHT
   $filter = applyAccessLevelFilter('m'); // With alias
   ```

2. **Mixing parameters**
   ```php
   // WRONG - Parameter name conflict
   $params = ['assembly_id' => 5]; // Conflicts with access_assembly_id
   
   // RIGHT - Use different names
   $params = ['filter_assembly_id' => 5];
   ```

3. **Not using WHERE 1=1**
   ```php
   // WRONG - If access filter is empty, SQL breaks
   $query = "SELECT * FROM members m" . $filter['where'];
   
   // RIGHT - WHERE 1=1 ensures valid SQL
   $query = "SELECT * FROM members m WHERE 1=1" . $filter['where'];
   ```

4. **Hardcoding user IDs**
   ```php
   // WRONG
   $query .= " AND assembly_id = 1";
   
   // RIGHT - Use helper functions
   $query .= getAccessLevelWhere('m');
   ```

---

## Benefits of This System

1. **Centralized Logic** - All access control in one place
2. **Easy to Apply** - Single function call adds filtering
3. **Consistent Behavior** - All modules follow same rules
4. **Maintainable** - Changes in one place affect everywhere
5. **Secure by Default** - Opt-in for viewing all data
6. **Flexible** - Options for custom column names
7. **Performance** - Database-level filtering (not PHP loops)

---

## Support

For questions or issues:
- Review this document
- Check `config/config.php` for function definitions
- Test with different user access levels
- Verify SQL queries are properly formed

---

**Last Updated:** 2025
**Version:** 1.0

Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists