Files
L-Ami-Fiduciaire/_bmad-output/implementation-artifacts/1-1-permission-configuration-and-controller-traits.md

231 lines
13 KiB
Markdown
Raw Normal View History

# Story 1.1: Permission Configuration & Controller Traits
Status: done
<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->
## 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.