Skip to content

🔐 Implement RBAC System (Role-Based Access Control) #5

@kevalyq

Description

@kevalyq

Feature Description

Implement a comprehensive Role-Based Access Control (RBAC) system to manage user permissions across the SecPal platform, including temporal role assignments with automatic expiration and role/permission management API.

User Stories

  • As an Admin, I want to assign roles to users so that they have appropriate access levels
  • As an Admin, I want to create custom roles so that I can tailor permissions to specific needs
  • As an Admin, I want to manage permissions so that I can control access granularly
  • As a Manager, I want to control who can view/edit employees in my branch so that data stays secure
  • As a Works Council member, I want restricted access to only relevant employee data so that privacy is protected
  • As a Client, I want read-only access to my location's data so that I can monitor services
  • As a Manager, I want to assign temporary roles with expiration dates for vacation coverage, projects, or events
  • As an Admin, I want roles to automatically expire to ensure principle of least privilege

Acceptance Criteria

Core RBAC

  • ADR-004 created documenting RBAC architecture decision (spatie/laravel-permission vs custom)
  • Database migrations for roles, permissions, role_user tables (Spatie provides these)
  • Predefined system roles created:
    • Admin (full access)
    • Manager (branch-scoped)
    • Guard (own data + assignments)
    • Client (read-only, location-scoped)
    • Works Council (approval workflows, limited employee access)
  • NEW: Role & Permission Management API
    • CRUD endpoints for roles (system roles protected)
    • CRUD endpoints for permissions
    • Hybrid approach: system roles + custom roles
    • Permission naming convention: resource.action
  • Policy classes implemented for major resources:
    • EmployeePolicy - who can view/edit employees
    • ShiftPlanPolicy - who can create/approve/publish shift plans
    • WorksCouncilPolicy - who can access BR-specific features
    • NEW: RoleManagementPolicy - who can manage roles
    • NEW: PermissionManagementPolicy - who can manage permissions
  • Permission middleware applied to all API routes
  • Scope-based access (branch, location, division)

Temporal Role Assignments

  • valid_from and valid_until columns added to user_roles table
  • auto_revoke flag to control automatic expiration behavior
  • Eloquent scope active() to filter roles by validity period
  • Scheduled command roles:expire runs every minute to revoke expired roles
  • API endpoints for assigning roles with temporal constraints:
    • POST /api/v1/users/{id}/roles - with optional valid_from, valid_until
    • GET /api/v1/users/{id}/roles - includes expiration info
    • DELETE /api/v1/users/{id}/roles/{role} - revoke role assignment
    • PATCH /api/v1/users/{id}/roles/{role}/extend - extend expiration
  • Notifications for role expiration:
    • 7 days before: notify manager
    • 24 hours before: notify user + manager
    • On expiration: notify both parties
  • Audit trail for role assignments (role_assignments_log table)

Role & Permission Management (Phase 4 - NEW)

  • Role CRUD API (7 endpoints):
    • List all roles (system + custom)
    • Create custom role
    • Get role details
    • Update role (system: permissions only, custom: all)
    • Delete custom role
    • Assign/revoke permissions to/from role
  • Permission CRUD API (5 endpoints):
    • List all permissions (grouped by resource)
    • Create permission
    • Get permission details
    • Update permission (description only)
    • Delete permission (if not assigned)
  • System vs Custom Roles:
    • System roles: Protected names, cannot be deleted
    • Custom roles: Fully manageable by Admin
    • is_system_role flag in database
  • Authorization:
    • Only Admin can manage roles/permissions
    • Policies enforce all operations

Testing & Quality

  • Unit tests for all policies (>80% coverage) - Phase 1-3
  • Integration tests for permission enforcement - Phase 1-3
  • Unit tests for temporal role logic (expiration, auto-revoke) - Phase 2
  • Feature tests for temporal API endpoints - Phase 3
  • Test timezone edge cases (UTC storage, user timezone display) - Phase 2
  • NEW: Feature tests for Role CRUD (12 tests)
  • NEW: Feature tests for Permission CRUD (8 tests)
  • NEW: Authorization tests for role management (6 tests)
  • NEW: Integration tests for custom role lifecycle
  • Documentation updated (API docs, README)

Technical Notes

Database Schema

// Base tables (Spatie Laravel-Permission)
roles:
  - id (BIGINT), name, guard_name
  - **NEW:** is_system_role (boolean, default: false)
  - created_at, updated_at

permissions:
  - id (BIGINT), name, guard_name
  - created_at, updated_at

role_has_permissions:
  - permission_id, role_id

model_has_roles (pivot table):
  - model_type, model_id (UUID), role_id, tenant_id (BIGINT)
  - **Phase 1 additions:**
  - valid_from (timestamp, nullable) - role becomes active
  - valid_until (timestamp, nullable) - role expires
  - auto_revoke (boolean, default: true) - auto-delete on expiry
  - assigned_by (BIGINT, foreign key to users) - who assigned the role
  - reason (text, nullable) - justification for assignment
  - INDEX: (valid_from, valid_until)

// Audit trail (Phase 2)
role_assignments_log:
  - id (UUID)
  - user_id (BIGINT)
  - role_id (BIGINT)
  - action (enum: 'assigned', 'revoked', 'expired', 'extended')
  - valid_from, valid_until (timestamp)
  - assigned_by (BIGINT)
  - reason (text)
  - created_at

Temporal Logic Implementation

// Eloquent Scope for active roles (Phase 1)
public function scopeActive($query)
{
    return $query->where(function ($q) {
        $q->whereNull('valid_from')
          ->orWhere('valid_from', '<=', now());
    })->where(function ($q) {
        $q->whereNull('valid_until')
          ->orWhere('valid_until', '>', now());
    });
}

// Scheduled command (runs every minute) - Phase 2
// Console/Commands/ExpireRoles.php
public function handle()
{
    $expired = TemporalRoleUser::expired()->cursor();
    
    foreach ($expired->chunk(100) as $chunk) {
        DB::transaction(function () use ($chunk) {
            foreach ($chunk as $assignment) {
                // 1. Delete assignment
                $deleted = DB::table('model_has_roles')
                    ->where('model_id', $assignment->model_id)
                    ->where('role_id', $assignment->role_id)
                    ->where('auto_revoke', true)
                    ->where('valid_until', '<', now())
                    ->delete();
                
                // 2. Log to audit trail (only if deleted)
                if ($deleted > 0) {
                    RoleAssignmentLog::create([
                        'user_id' => $assignment->model_id,
                        'role_id' => $assignment->role_id,
                        'action' => 'expired',
                        'valid_from' => $assignment->valid_from,
                        'valid_until' => $assignment->valid_until,
                    ]);
                }
            }
        });
    }
}

// Trait: HasTemporalRoles (Phase 1)
public function hasRole($role): bool
{
    return $this->roles()
        ->active() // Only consider valid roles
        ->where('name', $role)
        ->exists();
}

Notification Schedule

// Schedule in routes/console.php
Schedule::command('roles:notify-expiring --days=7')->daily();
Schedule::command('roles:notify-expiring --days=1')->daily();
Schedule::command('roles:expire')->everyMinute();

Example Permissions (Phase 4)

Format: resource.action

employees.read
employees.create
employees.update
employees.delete
employees.read_salary (restricted)
employees.read_all_branches (admin only)

shifts.read
shifts.create
shifts.update
shifts.delete
shifts.publish
shifts.approve_as_br (works council only)

work_instructions.read
work_instructions.create
work_instructions.update
work_instructions.delete
work_instructions.publish
work_instructions.acknowledge
work_instructions.view_acknowledgments

roles.read
roles.create
roles.update
roles.delete
roles.assign_temporary (manager/admin only)
roles.extend_expiration (manager/admin only)

permissions.read
permissions.create
permissions.update
permissions.delete

works_council.access_employee_files
works_council.approve_shift_plans

System Roles (Phase 4)

Roles with is_system_role=true are protected:

Role Description Can Delete? Can Rename? Can Change Permissions?
Admin Full system access ❌ No ❌ No ✅ Yes
Manager Branch management ❌ No ❌ No ✅ Yes
Guard Own data + shifts ❌ No ❌ No ✅ Yes
Client Read-only access ❌ No ❌ No ✅ Yes
Works Council BR workflows ❌ No ❌ No ✅ Yes

Custom roles (created via API):

Role Description Can Delete? Can Rename? Can Change Permissions?
Regional Manager Example custom ✅ Yes* ✅ Yes ✅ Yes

*Only if not assigned to any users

Spatie vs Custom Evaluation Criteria

Criterion Spatie Custom
Multi-tenancy support ✅ Good ✅ Full control
Scope-based permissions ⚠️ Requires customization ✅ Native
Works Council workflows ⚠️ Custom logic needed ✅ Tailored
Temporal assignments ⚠️ Requires pivot customization ✅ Native support
Auto-expiry ❌ Not built-in ✅ Full control
Role Management API 🟢 Use with extensions ✅ Native support
Complexity 🟢 Low 🟠 Medium
Maintenance 🟢 Community 🔴 Self

Decision (ADR-004): Hybrid approach using Spatie with custom temporal extensions and role management API

Use Cases for Temporal Roles

1. Vacation Coverage

Manager A on vacation (2025-12-01 to 2025-12-14)
→ Manager B gets Manager A's permissions temporarily
→ Auto-revoke on 2025-12-14 23:59:59 UTC

2. Project-Based Access

External auditor needs read access for 1 week
→ Assign "Auditor" role with valid_until = 2025-11-13 23:59:59
→ Notifications 24h before expiry
→ Auto-revoke on expiration

3. Event-Based Elevation

Guard becomes "Team Lead" for large event (2025-11-15 18:00 to 2025-11-16 06:00)
→ Elevated permissions during event only
→ Automatic revert to Guard role after event

4. Compliance (Principle of Least Privilege)

Developer needs production access for hotfix
→ Assign "Admin" role with valid_until = +2 hours
→ Auto-revoke ensures access is removed after fix
→ Audit trail logs access duration

5. Custom Role for Regional Manager (NEW)

Organization needs "Regional Manager" role
→ Admin creates custom role via API
→ Assigns permissions: employees.*, shifts.*, work_instructions.read
→ Assigns role to regional managers
→ Can modify role permissions as needs evolve

Important Considerations

Timezone Handling

  • Storage: All timestamps in UTC (database + application)
  • Display: Convert to user's timezone in API responses
  • Edge Cases: Document behavior when role expires during user's active session

Security

  • Only Manager and Admin roles can assign temporal roles
  • Only Admin role can manage roles/permissions (Phase 4)
  • Cannot extend expiration beyond original assigner's permissions
  • Audit log is immutable (no deletion allowed)
  • API validates valid_from < valid_until
  • System roles are protected from deletion
  • Custom roles can only be deleted if not assigned to users

Performance

  • Index on (valid_from, valid_until) for fast active role queries
  • Index on is_system_role for role management queries
  • Scheduled command processes in batches (100 roles per transaction) if >1000 expired roles
  • Cache active roles per user (invalidate on assignment/revocation)

Phase Progress

Dependencies

Reference

  • docs/feature-requirements.md - Section "RBAC & Permission System"
  • docs/legal-compliance.md - GDPR access control requirements
  • ADR-004: RBAC Architecture Decision (merged in .github repo)

Milestone

🎯 v0.2.0 - Core authentication/authorization

Labels

  • priority: blocker 🔴
  • type: feature
  • component: api
  • effort: XL (3-4 weeks with temporal features + role management)

Sub-issues

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions