Skip to content

User Direct Permission Assignment API (RBAC Phase 4) #138

@kevalyq

Description

@kevalyq

🎯 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 roles
    • direct: Permissions assigned directly to user
    • all: 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_table
    • valid_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_until if both present

Policy

  • Create UserPermissionPolicy.php
    • viewPermissions() → User can view own, Admin can view all
    • assignPermission() → Admin only
    • revokePermission() → Admin only
  • Register policy in AppServiceProvider

Routes

  • Add 4 routes in routes/api.php
  • All routes under auth:sanctum middleware

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:

⏱️ 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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions