- 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>
231 lines
13 KiB
Markdown
231 lines
13 KiB
Markdown
# 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.
|