-
Notifications
You must be signed in to change notification settings - Fork 0
Closed
Description
🎯 Objective
Implement Direct Permission Assignment API allowing permissions to be assigned directly to users, bypassing roles (RBAC Phase 4 - #108).
📋 Scope
User Direct Permission Endpoints
1. List All User Permissions
GET /api/v1/users/{id}/permissions
- Response includes:
via_roles: Permissions inherited from assigned rolesdirect: Permissions assigned directly to userall: Combined list (deduplicated)
- Authorization: User can view own, Admin can view all
2. Assign Direct Permission
POST /api/v1/users/{id}/permissions
- Bulk assignment support
- Optional temporal constraints:
valid_from(timestamp, nullable)valid_until(timestamp, nullable)
- Authorization: Admin only (
permissions.assign_direct)
3. Revoke Direct Permission
DELETE /api/v1/users/{id}/permissions/{permission}
- Only removes direct assignment (role-based permissions remain)
- Authorization: Admin only (
permissions.revoke_direct)
4. List Only Direct Permissions
GET /api/v1/users/{id}/permissions/direct
- Excludes permissions from roles
- Shows temporal constraints if present
- Authorization: User can view own, Admin can view all
🏗️ Implementation Checklist
Database Migration (Optional)
- Add migration:
add_temporal_columns_to_model_has_permissions_tablevalid_from(timestamp, nullable)valid_until(timestamp, nullable)- Only if temporal direct permissions are needed
- Can be added later if not needed initially
Controller
- Create
UserPermissionController.php - Implement
index()- List all permissions (via_roles + direct + all) - Implement
store()- Assign direct permissions (bulk + temporal) - Implement
destroy()- Revoke direct permission - Implement
direct()- List only direct permissions
Form Requests
- Create
AssignUserPermissionRequest.php- Validate: permissions array required
- Validate: permission names exist
- Validate: temporal constraints (optional)
- Validate:
valid_from<valid_untilif both present
Policy
- Create
UserPermissionPolicy.phpviewPermissions()→ User can view own, Admin can view allassignPermission()→ Admin onlyrevokePermission()→ Admin only
- Register policy in
AppServiceProvider
Routes
- Add 4 routes in
routes/api.php - All routes under
auth:sanctummiddleware
Tests
- Create
UserPermissionAssignmentApiTest.php - 10 comprehensive tests covering:
- List user permissions (via_roles + direct + all)
- Assign direct permission to user
- Revoke direct permission (role permissions remain)
- Direct permission with temporal constraints
- Permission inheritance (role + direct)
- View own permissions as user
- Admin can view any user's permissions
- User cannot view other user's permissions (403)
- 401 Unauthorized
- 422 Validation errors
Quality Gates
- PHPStan Level Max: 0 errors
- Laravel Pint: Clean (
--test --dirty) - All tests pass locally
- REUSE compliance
✅ Acceptance Criteria
- 4 RESTful endpoints functional
- Direct permissions work independently of roles
- Permission inheritance (role + direct) works correctly
- Temporal constraints (optional) functional
- User can view own permissions
- Admin can view/assign/revoke any user's permissions
- 10 comprehensive tests (all passing)
- PHPStan + Pint clean
📊 Response Structure Examples
List All Permissions:
{
"data": {
"via_roles": [
{"name": "employees.read", "role": "Manager"},
{"name": "shifts.read", "role": "Manager"}
],
"direct": [
{
"name": "employees.export",
"valid_from": null,
"valid_until": null,
"assigned_at": "2025-11-09T10:00:00Z",
"assigned_by": "admin@secpal.app"
},
{
"name": "reports.generate",
"valid_from": "2025-11-01T00:00:00Z",
"valid_until": "2025-11-30T23:59:59Z",
"assigned_at": "2025-11-09T10:00:00Z",
"assigned_by": "admin@secpal.app"
}
],
"all": ["employees.read", "shifts.read", "employees.export", "reports.generate"]
}
}Assign Direct Permission (Temporal):
POST /api/v1/users/123/permissions
{
"permissions": ["reports.generate"],
"valid_from": "2025-11-01T00:00:00Z",
"valid_until": "2025-11-30T23:59:59Z"
}🔗 Related Issues
Part of: #108 - RBAC Phase 4
Depends on:
- PR [DRAFT] feat: RBAC Phase 4 - Role Management CRUD API #131 (Role Management API merged)
- Next sub-issue (Permission Management API)
⏱️ Effort Estimate
Time: 4-5 hours
Breakdown:
- Migration (optional): 0.5h
- Controller + Form Requests: 2h
- Policy + Routes: 0.5h
- Tests: 1.5h
- Quality gates: 0.5h
Complexity: Medium (permission inheritance logic, temporal constraints)
🎯 Use Cases for Direct Permissions
| Scenario | Solution | Why Direct Permission? |
|---|---|---|
| Guard needs temporary export access | Assign employees.export directly for 1 week |
Don't want to modify Guard role or create new role |
| Manager should NOT delete employees | Revoke employees.delete directly |
Override role permission |
| Client needs report generation | Assign reports.generate directly |
Special exception for one client |
| Auditor needs read access | Assign multiple read permissions | Time-limited access without creating "Auditor" role |
📝 Implementation Notes
Permission Hierarchy
User Permissions = Role Permissions ∪ Direct Permissions
Example:
User "John" has role "Manager":
- Role "Manager" grants: [employees.read, employees.update, shifts.*]
- Direct permissions: [employees.export, reports.generate]
- Total permissions: [employees.read, employees.update, shifts.*, employees.export, reports.generate]
If "Manager" role is removed:
- Direct permissions remain: [employees.export, reports.generate]
Temporal Constraints (Optional)
If implemented:
- Permissions expire automatically after
valid_until - Background job checks and removes expired permissions
- Frontend shows expiration date in UI
If not implemented initially:
- Can be added later via migration
- Direct permissions are permanent until manually revoked
📝 Review Checklist
Before creating PR:
- Permission inheritance logic correct
- Direct permissions independent of roles
- Temporal constraints functional (if implemented)
- Authorization checks user vs admin
- Test covers all permission combinations
- Guard name explicit (
sanctum) in tests
Created: 2025-11-09
Category: Feature / RBAC
Size: ~500-600 LOC (excluding tests)
Risk: Medium (complex permission inheritance logic)
Metadata
Metadata
Assignees
Labels
No labels
Type
Projects
Status
✅ Done