Skip to content

Predefined Roles \u0026 Permissions Seeder (Idempotent) - RBAC Phase 4 #139

@kevalyq

Description

@kevalyq

🎯 Objective

Create idempotent seeder for predefined roles and permissions (RBAC Phase 4 - #108).

📋 Scope

Predefined Roles

  1. Admin - Full access to all resources
  2. Manager - Branch-scoped management (employees, shifts, work instructions)
  3. Guard - Own data + shift assignments
  4. Client - Read-only access to location data
  5. Works Council (Betriebsrat) - BR-specific permissions

Permission Groups

Employees:

  • employees.read, employees.create, employees.update, employees.delete
  • employees.read_salary, employees.read_all_branches, employees.export

Shifts:

  • shifts.read, shifts.create, shifts.update, shifts.delete
  • shifts.publish, shifts.approve_as_br

Work Instructions:

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

Roles:

  • roles.read, roles.create, roles.update, roles.delete
  • roles.assign_temporary, roles.extend_expiration

Permissions:

  • permissions.read, permissions.create, permissions.update, permissions.delete
  • permissions.assign_direct, permissions.revoke_direct

Works Council:

  • works_council.access_employee_files, works_council.approve_shift_plans

Reports:

  • reports.generate, reports.view, reports.export

🏗️ Implementation Checklist

Seeder

  • Create RolesAndPermissionsSeeder.php
  • Idempotent design - uses firstOrCreate()
  • Create all permissions first
  • Create predefined roles
  • Assign permissions to roles
  • Add to DatabaseSeeder.php call chain

Idempotency Rules

  • Uses firstOrCreate() - safe to run multiple times
  • Skips if role/permission already exists
  • Updates permissions only if role has none
  • Does not overwrite existing data

Role Permission Matrix

  • Admin: All permissions (*)
  • Manager:
    • employees.*, shifts.*, work_instructions.*
    • Excludes: roles.*, permissions.*, employees.read_all_branches
  • Guard:
    • employees.read (own only), shifts.read, work_instructions.read
  • Client:
    • shifts.read (location-scoped)
  • Works Council:
    • employees.read, shifts.read, shifts.approve_as_br
    • works_council.*

Documentation

  • Add PHPDoc explaining idempotency
  • Comment on each role's purpose
  • Document permission naming convention

Quality Gates

  • PHPStan Level Max: 0 errors
  • Laravel Pint: Clean
  • Seeder runs successfully
  • REUSE compliance

✅ Acceptance Criteria

  • Seeder creates 5 predefined roles with correct permissions
  • Seeder is idempotent (can run multiple times safely)
  • Deleted predefined roles are recreated on next seeder run
  • All permissions use guard_name='sanctum'
  • No errors when running seeder repeatedly
  • PHPStan + Pint clean

📊 Seeder Code Example

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class RolesAndPermissionsSeeder extends Seeder
{
    /**
     * Run the seeder.
     * 
     * This seeder is IDEMPOTENT - safe to run multiple times.
     * Uses firstOrCreate() to skip existing roles/permissions.
     * If role deleted, next run recreates it.
     */
    public function run(): void
    {
        // Create permissions first
        $permissions = [
            // Employees
            'employees.read', 'employees.create', 'employees.update', 'employees.delete',
            'employees.read_salary', 'employees.read_all_branches', 'employees.export',
            
            // Shifts
            'shifts.read', 'shifts.create', 'shifts.update', 'shifts.delete',
            'shifts.publish', 'shifts.approve_as_br',
            
            // ... (full list)
        ];

        foreach ($permissions as $permissionName) {
            Permission::firstOrCreate(
                ['name' => $permissionName, 'guard_name' => 'sanctum']
            );
        }

        // Create Admin role
        $admin = Role::firstOrCreate(
            ['name' => 'Admin', 'guard_name' => 'sanctum'],
            ['description' => 'Full system access']
        );

        // Sync permissions only if role has none
        if ($admin->permissions()->count() === 0) {
            $admin->syncPermissions(Permission::all());
        }

        // Repeat for Manager, Guard, Client, Works Council...
    }
}

🔗 Related Issues

Part of: #108 - RBAC Phase 4
Depends on:

⏱️ Effort Estimate

Time: 2-3 hours
Breakdown:

  • Define permission list: 0.5h
  • Seeder implementation: 1h
  • Testing idempotency: 0.5h
  • Documentation: 0.5h

Complexity: Low (straightforward data seeding)

🎯 Implementation Notes

Why Idempotent?

Benefits:

  1. ✅ Safe to run in production
  2. ✅ Safe to run multiple times (CI/CD)
  3. ✅ Automatically recreates deleted roles
  4. ✅ No duplicate entries

Protection:

  • Roles protected by assignment status (cannot delete if assigned)
  • Seeder ensures predefined roles always available (recreates if deleted)
  • No runtime restrictions on modification (rename/change permissions freely)

Permission Naming Convention

Format: resource.action

Resources:

  • employees, shifts, work_instructions, roles, permissions
  • works_council, reports

Common Actions:

  • read, create, update, delete, export

Special Actions:

  • employees.read_salary - View salary data
  • employees.read_all_branches - Cross-branch access
  • shifts.approve_as_br - Works council approval
  • roles.assign_temporary - Assign temporal roles

Testing Idempotency

# Run seeder 3 times
ddev exec php artisan db:seed --class=RolesAndPermissionsSeeder
ddev exec php artisan db:seed --class=RolesAndPermissionsSeeder
ddev exec php artisan db:seed --class=RolesAndPermissionsSeeder

# Verify: No duplicates, no errors
ddev exec php artisan tinker
>>> Role::count()  // Should be 5
>>> Permission::count()  // Should be X (total permissions)

📝 Review Checklist

Before creating PR:

  • Seeder uses firstOrCreate() everywhere
  • Guard name explicit (sanctum) for all permissions
  • Permission naming follows resource.action convention
  • All 5 predefined roles have correct permissions
  • Tested running seeder 3+ times (no duplicates)
  • PHPDoc explains idempotency

Created: 2025-11-09
Category: Database / RBAC
Size: ~200-300 LOC (seeder + documentation)
Risk: Low (data seeding, no business logic)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions