# 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 file** — `config/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 enum** — `app/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` trait** — `app/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` trait** — `app/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 `member` → `worker` in `workspace_user.role` ## Tasks / Subtasks - [x] Task 1: Rename `Member` to `Worker` in `WorkspaceUserRole` enum (AC: #3) - [x] 1.1 Update `app/Enums/WorkspaceUserRole.php`: rename `Member` case to `Worker` with value `'worker'` - [x] 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` - [x] 1.3 Update `WorkspaceUser` model default role to `WorkspaceUserRole::Worker` (N/A — no default role in model; updated in original migration) - [x] 1.4 Update `WorkspaceUserFactory` default role to `WorkspaceUserRole::Worker` (N/A — no WorkspaceUserFactory exists) - [x] 1.5 Update `DatabaseSeeder` — change any `member` references to `worker` - [x] 1.6 Search codebase for any `Member` / `member` references in role context and update (updated: WorkspaceController, tests, WorkspaceForm.vue) - [x] Task 2: Create Permission enum (AC: #2) - [x] 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'` - [x] Task 3: Create permission config file (AC: #1) - [x] 3.1 Create `config/permissions.php` with `defaults` array keyed by role - [x] Task 4: Create `HasWorkspaceScope` trait (AC: #4) - [x] 4.1 Create `app/Concerns/HasWorkspaceScope.php` with `currentWorkspace()` and `authorizeWorkspaceAccess()` methods - [x] 4.2 `currentWorkspace()` retrieves workspace via `session('current_workspace_id')` and `auth()->user()->workspaces()` relationship - [x] 4.3 `authorizeWorkspaceAccess()` compares resource `workspace_id` to current workspace, `abort(404)` on mismatch - [x] Task 5: Create `AuthorizesPermissions` trait (AC: #5, #6) - [x] 5.1 Create `app/Concerns/AuthorizesPermissions.php` - [x] 5.2 Implement `authorizePermission(string $permission)` with Owner/Worker/Manager branching - [x] 5.3 Manager branch reads `permissions` JSON column from `WorkspaceUser` pivot — `abort(404)` if key missing or `false` - [x] Task 6: Write tests (AC: #7) - [x] 6.1 Create `tests/Unit/PermissionCheckTest.php` — test `AuthorizesPermissions` logic for all 3 roles + unknown keys - [x] 6.2 Create `tests/Feature/Team/WorkspaceScopeTest.php` — test `HasWorkspaceScope` trait - [x] 6.3 Create `tests/Feature/Database/MemberToWorkerMigrationTest.php` — test data migration - [x] 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 `Member` → `Worker` - **`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 ```php // 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: ```php 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 ```php // 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 `Member` → `Worker` 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.