Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/1-1-permission-configuration-and-controller-traits.md
Saad Ibn-Ezzoubayr c89d1879bf feat: complete Epic 1 — team management & permission system
- Story 1.1: Permission enum, config, AuthorizesPermissions & HasWorkspaceScope traits, member→worker migration
- Story 1.2: Team page with member list, invitation system with queued email
- Story 1.3: Role assignment (Manager/Worker) and member removal with activity logging
- Story 1.4: Owner-only permission toggle matrix for Managers (manage team, view logs, configure portal)
- Story 1.5: Role-based access enforcement — Workers see only assigned declarations/clients, sidebar scoping
- Story 1.6: Workspace switcher dropdown for multi-workspace users with session-based switching
- 83 new/modified files, 182 tests passing with zero regressions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 00:12:50 +00:00

13 KiB

Story 1.1: Permission Configuration & Controller Traits

Status: done

Story

As a developer, I want a centralized permission configuration and reusable controller traits for workspace scoping and permission checking, So that all future controllers can enforce role-based access consistently with minimal code duplication.

Acceptance Criteria

  1. Permission config fileconfig/permissions.php defines default permissions per role:

    • Owner: ['*'] (all permissions)
    • Manager: configurable defaults (can_manage_team: false, can_view_activity_logs: true, can_configure_portal: false)
    • Worker: [] (no configurable permissions)
  2. Permission enumapp/Enums/Permission.php exists with all permission keys as snake_case values (can_manage_team, can_view_activity_logs, can_configure_portal)

  3. Rename Member to Worker — The existing WorkspaceUserRole enum value Member must be renamed to Worker across models, migrations, seeders, and any references. A data migration updates existing member values in workspace_user.role to worker.

  4. HasWorkspaceScope traitapp/Concerns/HasWorkspaceScope.php provides:

    • currentWorkspace(): Workspace — resolves workspace from session current_workspace_id
    • authorizeWorkspaceAccess(): void — verifies resource belongs to current workspace, abort(404) if not
  5. AuthorizesPermissions traitapp/Concerns/AuthorizesPermissions.php provides:

    • authorizePermission(string $permission): void
    • Owner: always passes (returns immediately)
    • Worker: always fails (abort(404))
    • Manager: checks workspace_user.permissions JSON column — abort(404) if key is false or absent
    • Unknown permission keys default to false
  6. Authorization failures return abort(404) — never abort(403), per architecture convention (NFR9)

  7. Unit tests verify:

    • Permission checking logic for all three roles (Owner passes, Worker fails, Manager checks JSON)
    • Unknown permission keys default to false for Managers
    • HasWorkspaceScope resolves workspace correctly from session
    • Data migration renames memberworker in workspace_user.role

Tasks / Subtasks

  • Task 1: Rename Member to Worker in WorkspaceUserRole enum (AC: #3)
    • 1.1 Update app/Enums/WorkspaceUserRole.php: rename Member case to Worker with value 'worker'
    • 1.2 Create data migration database/migrations/2026_03_14_000001_rename_member_to_worker_in_workspace_user.php — updates existing member values to worker
    • 1.3 Update WorkspaceUser model default role to WorkspaceUserRole::Worker (N/A — no default role in model; updated in original migration)
    • 1.4 Update WorkspaceUserFactory default role to WorkspaceUserRole::Worker (N/A — no WorkspaceUserFactory exists)
    • 1.5 Update DatabaseSeeder — change any member references to worker
    • 1.6 Search codebase for any Member / member references in role context and update (updated: WorkspaceController, tests, WorkspaceForm.vue)
  • Task 2: Create Permission enum (AC: #2)
    • 2.1 Create app/Enums/Permission.php using bensampo/laravel-enum with values: CanManageTeam = 'can_manage_team', CanViewActivityLogs = 'can_view_activity_logs', CanConfigurePortal = 'can_configure_portal'
  • Task 3: Create permission config file (AC: #1)
    • 3.1 Create config/permissions.php with defaults array keyed by role
  • Task 4: Create HasWorkspaceScope trait (AC: #4)
    • 4.1 Create app/Concerns/HasWorkspaceScope.php with currentWorkspace() and authorizeWorkspaceAccess() methods
    • 4.2 currentWorkspace() retrieves workspace via session('current_workspace_id') and auth()->user()->workspaces() relationship
    • 4.3 authorizeWorkspaceAccess() compares resource workspace_id to current workspace, abort(404) on mismatch
  • Task 5: Create AuthorizesPermissions trait (AC: #5, #6)
    • 5.1 Create app/Concerns/AuthorizesPermissions.php
    • 5.2 Implement authorizePermission(string $permission) with Owner/Worker/Manager branching
    • 5.3 Manager branch reads permissions JSON column from WorkspaceUser pivot — abort(404) if key missing or false
  • Task 6: Write tests (AC: #7)
    • 6.1 Create tests/Unit/PermissionCheckTest.php — test AuthorizesPermissions logic for all 3 roles + unknown keys
    • 6.2 Create tests/Feature/Team/WorkspaceScopeTest.php — test HasWorkspaceScope trait
    • 6.3 Create tests/Feature/Database/MemberToWorkerMigrationTest.php — test data migration
    • 6.4 Run full test suite: composer test — 105 passed, 0 failures

Dev Notes

Architecture Constraints (MUST FOLLOW)

  • Enum library: Use bensampo/laravel-enum ^6.12 (NOT native PHP enums). All existing enums follow this pattern.
  • Model casts: Use method-based protected function casts(): array (NEVER $casts property)
  • Authorization: Always abort(404) for permission failures (NEVER abort(403)) — intentional to hide workspace existence
  • No Policies/Gates: This project uses custom authorizeXxx() methods in traits/controllers, NOT Laravel Policies or Gates
  • No Spatie Permission package: Permissions use the JSON column on workspace_user pivot — do NOT install spatie/laravel-permission
  • Mass assignment: Explicit $fillable arrays (NEVER $guarded = [])
  • Workspace scoping: Always from session('current_workspace_id'), never from request params

Existing Code to Build On

  • WorkspaceUser model (app/Models/WorkspaceUser.php): Already extends Pivot, has role column, and permissions JSON column (added in Story 0.5). Cast permissions to array if not already done.
  • WorkspaceUserRole enum (app/Enums/WorkspaceUserRole.php): Currently has Owner, Manager, Member — rename MemberWorker
  • EnsureUserHasWorkspace middleware (app/Http/Middleware/EnsureUserHasWorkspace.php): Already validates workspace access via session. The new HasWorkspaceScope trait complements this (middleware checks access, trait provides controller helpers).
  • DeclarationController/ClientController: Already use abort(404) pattern with authorizeDeclaration()/authorizeClient() methods — the new traits will replace these inline patterns.
  • Existing traits location: app/Concerns/ (contains PasswordValidationRules.php, ProfileValidationRules.php)

Permission Checking Implementation Pattern

// app/Concerns/AuthorizesPermissions.php
protected function authorizePermission(string $permission): void
{
    $workspaceUser = auth()->user()->currentWorkspaceUser();

    if ($workspaceUser->role === WorkspaceUserRole::Owner) {
        return; // Owners can do everything
    }

    if ($workspaceUser->role === WorkspaceUserRole::Worker) {
        abort(404); // Workers never have configurable permissions
    }

    // Manager: check JSON permissions column
    if (!($workspaceUser->permissions[$permission] ?? false)) {
        abort(404); // 404 not 403 per project convention
    }
}

User Model Requirements

The User model needs a currentWorkspaceUser(): WorkspaceUser method (if not already present) that returns the pivot record for the current workspace. Check if this exists; if not, add it:

public function currentWorkspaceUser(): WorkspaceUser
{
    return WorkspaceUser::where('user_id', $this->id)
        ->where('workspace_id', session('current_workspace_id'))
        ->firstOrFail();
}

Data Migration for Member → Worker Rename

// Migration up(): Update existing 'member' values to 'worker'
DB::table('workspace_user')
    ->where('role', 'member')
    ->update(['role' => 'worker']);

// Migration down(): Revert 'worker' back to 'member'
DB::table('workspace_user')
    ->where('role', 'worker')
    ->update(['role' => 'member']);

Project Structure Notes

  • Traits go in app/Concerns/ (established pattern)
  • Enums go in app/Enums/ (established pattern)
  • Config files go in config/ (standard Laravel)
  • Feature tests in tests/Feature/Team/ and tests/Feature/Database/
  • Unit tests in tests/Unit/
  • No new middleware needed — traits are used inside controllers, not as middleware

Testing Standards

  • Framework: Pest 4 with test() closures and expect() assertions
  • RefreshDatabase: Auto-applied via Pest.php — do NOT add manually
  • Run command: composer test (clears config → runs Pint → runs tests)
  • Test both: Happy path (authorized) AND sad path (unauthorized → 404)
  • Factory usage: Use WorkspaceUser::factory() to create test users with specific roles and permissions
  • Route helper: Use route() helper, never hardcoded URLs

References

  • [Source: _bmad-output/planning-artifacts/epics.md#Epic-1 — Story 1.1 requirements]
  • [Source: _bmad-output/planning-artifacts/architecture.md#D1-Permission-Toggle-Storage — JSON column decision]
  • [Source: _bmad-output/planning-artifacts/architecture.md#Permission-Checking-Pattern — authorizePermission() code pattern]
  • [Source: _bmad-output/planning-artifacts/architecture.md#Phase-1-Files — file locations]
  • [Source: _bmad-output/implementation-artifacts/0-5-*.md — previous story patterns and learnings]

Previous Story Intelligence (Epic 0 Learnings)

  • Enum convention: bensampo/laravel-enum with labels(): array for French display and custom methods
  • Observer pattern: Register in AppServiceProvider::boot() using Model::observe()
  • FK constraints: Use explicit ->on('table_name') (never bare ->constrained())
  • Scope discipline: Only modify files directly required by acceptance criteria — no cosmetic changes
  • Test organization: Group by domain (tests/Feature/Team/, tests/Feature/Database/)
  • Code review feedback: Business logic belongs in models/observers/traits, not controllers

Git Intelligence

Recent commits show Epic 0 is complete. Branch l-ami-fiduciaire-v1.0.0 has 4 commits. All Epic 0 stories implemented and retrospective done.

Dev Agent Record

Agent Model Used

Claude Opus 4.6

Debug Log References

  • bensampo/laravel-enum uses ->is() for comparisons, not === (Enum instance vs string constant). Fixed in AuthorizesPermissions trait.
  • permissions column already cast to array on WorkspaceUser model — do not json_encode() when attaching via relationship (causes double-encoding).
  • Unit tests in tests/Unit/ need explicit uses(Tests\TestCase::class, RefreshDatabase::class) since Pest.php only auto-applies these for Feature tests.
  • Added new migration shifted rollback step count in RenameFoldersToDeclarationsTest (5 → 6).

Completion Notes List

  • Ultimate context engine analysis completed — comprehensive developer guide created
  • Renamed MemberWorker across enum, migrations, seeders, controllers, tests, and frontend
  • Created Permission enum with 3 permission keys (CanManageTeam, CanViewActivityLogs, CanConfigurePortal)
  • Created config/permissions.php with role-based default permissions
  • Created HasWorkspaceScope trait for workspace resolution and resource access verification
  • Created AuthorizesPermissions trait for role-based permission checking (Owner/Worker/Manager)
  • Added currentWorkspaceUser() method to User model
  • Created 12 new tests (6 unit + 4 feature/team + 2 feature/database), all passing
  • Full test suite: 105 tests, 255 assertions, 0 failures

File List

New files:

  • app/Enums/Permission.php
  • app/Concerns/HasWorkspaceScope.php
  • app/Concerns/AuthorizesPermissions.php
  • config/permissions.php
  • database/migrations/2026_03_14_000001_rename_member_to_worker_in_workspace_user.php
  • tests/Unit/PermissionCheckTest.php
  • tests/Feature/Team/WorkspaceScopeTest.php
  • tests/Feature/Database/MemberToWorkerMigrationTest.php

Modified files:

  • app/Enums/WorkspaceUserRole.php (Member → Worker)
  • app/Models/User.php (added currentWorkspaceUser() method, added 'permissions' to withPivot)
  • app/Http/Controllers/WorkspaceController.php (Member → Worker references)
  • database/migrations/2026_02_26_220354_create_workspace_user_table.php (default role Member → Worker)
  • database/seeders/DatabaseSeeder.php (Member → Worker references)
  • resources/js/components/WorkspaceForm.vue (defaultRole 'member' → 'worker')
  • tests/Feature/Notification/DeclarationMentionTest.php ('member' → 'worker' role strings)
  • tests/Feature/Declaration/MediaDownloadTest.php ('member' → 'worker' role string)
  • tests/Feature/Database/RenameFoldersToDeclarationsTest.php (rollback step count 5 → 6)

Change Log

  • 2026-03-14: Story 1.1 implemented — Permission configuration, controller traits (HasWorkspaceScope, AuthorizesPermissions), Permission enum, Member→Worker rename with data migration. All 105 tests passing.
  • 2026-03-15: Code review fixes — [H1] Added 'permissions' to withPivot() in User→workspaces relationship. [M1] Rewrote MemberToWorkerMigrationTest to invoke actual migration file instead of duplicating SQL inline. [M2] Noted: config/permissions.php defaults are defined but not consumed — by design, future stories will apply defaults. All 105 tests passing.