- 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>
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
-
Permission config file —
config/permissions.phpdefines 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)
- Owner:
-
Permission enum —
app/Enums/Permission.phpexists with all permission keys as snake_case values (can_manage_team,can_view_activity_logs,can_configure_portal) -
Rename
MembertoWorker— The existingWorkspaceUserRoleenum valueMembermust be renamed toWorkeracross models, migrations, seeders, and any references. A data migration updates existingmembervalues inworkspace_user.roletoworker. -
HasWorkspaceScopetrait —app/Concerns/HasWorkspaceScope.phpprovides:currentWorkspace(): Workspace— resolves workspace from sessioncurrent_workspace_idauthorizeWorkspaceAccess(): void— verifies resource belongs to current workspace,abort(404)if not
-
AuthorizesPermissionstrait —app/Concerns/AuthorizesPermissions.phpprovides:authorizePermission(string $permission): void- Owner: always passes (returns immediately)
- Worker: always fails (
abort(404)) - Manager: checks
workspace_user.permissionsJSON column —abort(404)if key isfalseor absent - Unknown permission keys default to
false
-
Authorization failures return
abort(404)— neverabort(403), per architecture convention (NFR9) -
Unit tests verify:
- Permission checking logic for all three roles (Owner passes, Worker fails, Manager checks JSON)
- Unknown permission keys default to
falsefor Managers HasWorkspaceScoperesolves workspace correctly from session- Data migration renames
member→workerinworkspace_user.role
Tasks / Subtasks
- Task 1: Rename
MembertoWorkerinWorkspaceUserRoleenum (AC: #3)- 1.1 Update
app/Enums/WorkspaceUserRole.php: renameMembercase toWorkerwith value'worker' - 1.2 Create data migration
database/migrations/2026_03_14_000001_rename_member_to_worker_in_workspace_user.php— updates existingmembervalues toworker - 1.3 Update
WorkspaceUsermodel default role toWorkspaceUserRole::Worker(N/A — no default role in model; updated in original migration) - 1.4 Update
WorkspaceUserFactorydefault role toWorkspaceUserRole::Worker(N/A — no WorkspaceUserFactory exists) - 1.5 Update
DatabaseSeeder— change anymemberreferences toworker - 1.6 Search codebase for any
Member/memberreferences in role context and update (updated: WorkspaceController, tests, WorkspaceForm.vue)
- 1.1 Update
- Task 2: Create Permission enum (AC: #2)
- 2.1 Create
app/Enums/Permission.phpusingbensampo/laravel-enumwith values:CanManageTeam = 'can_manage_team',CanViewActivityLogs = 'can_view_activity_logs',CanConfigurePortal = 'can_configure_portal'
- 2.1 Create
- Task 3: Create permission config file (AC: #1)
- 3.1 Create
config/permissions.phpwithdefaultsarray keyed by role
- 3.1 Create
- Task 4: Create
HasWorkspaceScopetrait (AC: #4)- 4.1 Create
app/Concerns/HasWorkspaceScope.phpwithcurrentWorkspace()andauthorizeWorkspaceAccess()methods - 4.2
currentWorkspace()retrieves workspace viasession('current_workspace_id')andauth()->user()->workspaces()relationship - 4.3
authorizeWorkspaceAccess()compares resourceworkspace_idto current workspace,abort(404)on mismatch
- 4.1 Create
- Task 5: Create
AuthorizesPermissionstrait (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
permissionsJSON column fromWorkspaceUserpivot —abort(404)if key missing orfalse
- 5.1 Create
- Task 6: Write tests (AC: #7)
- 6.1 Create
tests/Unit/PermissionCheckTest.php— testAuthorizesPermissionslogic for all 3 roles + unknown keys - 6.2 Create
tests/Feature/Team/WorkspaceScopeTest.php— testHasWorkspaceScopetrait - 6.3 Create
tests/Feature/Database/MemberToWorkerMigrationTest.php— test data migration - 6.4 Run full test suite:
composer test— 105 passed, 0 failures
- 6.1 Create
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$castsproperty) - Authorization: Always
abort(404)for permission failures (NEVERabort(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_userpivot — do NOT installspatie/laravel-permission - Mass assignment: Explicit
$fillablearrays (NEVER$guarded = []) - Workspace scoping: Always from
session('current_workspace_id'), never from request params
Existing Code to Build On
WorkspaceUsermodel (app/Models/WorkspaceUser.php): Already extendsPivot, hasrolecolumn, andpermissionsJSON column (added in Story 0.5). Castpermissionstoarrayif not already done.WorkspaceUserRoleenum (app/Enums/WorkspaceUserRole.php): Currently hasOwner,Manager,Member— renameMember→WorkerEnsureUserHasWorkspacemiddleware (app/Http/Middleware/EnsureUserHasWorkspace.php): Already validates workspace access via session. The newHasWorkspaceScopetrait complements this (middleware checks access, trait provides controller helpers).DeclarationController/ClientController: Already useabort(404)pattern withauthorizeDeclaration()/authorizeClient()methods — the new traits will replace these inline patterns.- Existing traits location:
app/Concerns/(containsPasswordValidationRules.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/andtests/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 andexpect()assertions RefreshDatabase: Auto-applied viaPest.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-enumwithlabels(): arrayfor French display and custom methods - Observer pattern: Register in
AppServiceProvider::boot()usingModel::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. permissionscolumn already cast toarrayon WorkspaceUser model — do notjson_encode()when attaching via relationship (causes double-encoding).- Unit tests in
tests/Unit/need explicituses(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→Workeracross enum, migrations, seeders, controllers, tests, and frontend - Created
Permissionenum with 3 permission keys (CanManageTeam, CanViewActivityLogs, CanConfigurePortal) - Created
config/permissions.phpwith role-based default permissions - Created
HasWorkspaceScopetrait for workspace resolution and resource access verification - Created
AuthorizesPermissionstrait 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.