Aller au contenu

Story CP-014: Permission Resolution + Module Gating

Module: control-plane Slice: Slice 3 from architecture (split from CP-003) Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 3 Depends on: CP-003 Estimated complexity: M


Objective

Implement the permission resolution engine: resolve a user's effective permissions by intersecting their role-based permissions with active tenant modules, and filtering out write permissions for suspended tenants. This is the backend-only "brain" that gates every API call. Provides the GET /api/v1/auth/my-permissions endpoint and the PermissionService + PermissionCacheService used by all authorization checks.


Backend Scope

Entities & Records

This story consumes entities defined in CP-003 (User, UserTenantMembership, MembershipRole, Role, Permission, RolePermission). No new entities are created.

Permission entity (defined in CP-003, reference):

Field Type Constraints
id UUID v7 PK
code String NOT NULL, unique (e.g., "cp.employee.create")
module_code String NOT NULL (e.g., "CP", "ABSMGT")
description String NOT NULL

Permission format: {module_code}.{entity}.{action} (e.g., cp.employee.create, absence.leave_request.approve)

DTOs:

public record MyPermissionsResponse(List<String> permissions) {}
// e.g., ["cp.employee.create", "cp.employee.read", "absence.leave_request.create"]

Repository Layer

Uses repositories from CP-003: - PermissionRepository.javafindByRoleIds(Set<UUID>) → List<Permission> - RoleRepository.javafindByTenantIdAndCode(UUID, String) → Optional<Role> - UserTenantMembershipRepository.javafindByUserIdAndTenantId(UUID, UUID) → Optional<UserTenantMembership>

Service Layer

PermissionService.java: - getPermissionsForUser(UUID userId, UUID tenantId) → List<String> — Resolves permissions: 1. Get user's roles for this tenant (from membership) 2. Get all permissions for those roles 3. Filter: only include permissions where the corresponding module is ACTIVE for the tenant (module gating — depends on CP-007, stub initially) 4. Filter: if tenant is SUSPENDED, remove all write permissions

PermissionCacheService.java: - Caffeine cache: key = userId:tenantId, value = List<String> permissions - TTL: 5 minutes - Invalidated on: role change, module activation/deactivation, tenant status change - Cache invalidation via Spring Application Events

API Endpoints

Method Path Request Body Response Auth
GET /api/v1/auth/my-permissions MyPermissionsResponse Any authenticated

Validation Rules

  • User must have an active membership in the current tenant
  • Module gating: permissions for inactive modules are silently excluded (no error, just absent)
  • SUSPENDED tenant: write permissions (.create, .update, .delete) removed; read permissions (.read, *.list) retained

Multi-Tenant Considerations

  • Permission resolution always happens in the context of a specific tenant (from TenantContext)
  • A user may have different roles (and thus different permissions) in different tenants
  • Module activation status is per-tenant (from CP-007 — stub with all-active until CP-007 is implemented)

Frontend Scope

None — this story is backend-only. Frontend consumes my-permissions via the auth context established in CP-001.


Acceptance Criteria

AC-020: Get my permissions (module-gated)
Given I have role EMPLOYE and my tenant has modules ABSMGT=ACTIVE and PAYROL=INACTIVE
When I call GET /api/v1/auth/my-permissions
Then I receive permissions for ABSMGT (e.g., "absence.leave_request.create")
And I do NOT receive permissions for PAYROL

AC-021: Permission denied for suspended tenant writes
Given my tenant is SUSPENDED
When I call GET /api/v1/auth/my-permissions
Then all write permissions are removed
And read permissions remain

AC-022: Assign role with inactive module permissions
Given I assign role RH_MANAGER which includes permissions for PAYROL (inactive module)
When the role is saved
Then the PAYROL permissions are silently ignored (not shown in my-permissions, 403 on API calls)

OHADA & Regulatory Rules

  • Loi 2013-450 (Art. 36): Permission data determines access to personal data. The permission resolution engine enforces the principle of least privilege — users only see data for modules their tenant has activated.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Service layer, §Caching, §Application Events
  • docs/standards/api-guidelines.md — §Authentication endpoints, §Standard envelope

Testing Requirements

Unit Tests

  • PermissionService: role → permissions resolution with module gating
  • PermissionService: SUSPENDED tenant → write permissions removed, read retained
  • PermissionService: multiple roles → permissions merged (union)
  • PermissionCacheService: cache hit returns cached value
  • PermissionCacheService: cache miss triggers resolution
  • PermissionCacheService: invalidation on role change event
  • PermissionCacheService: invalidation on module activation/deactivation event
  • PermissionCacheService: invalidation on tenant status change event

Integration Tests

  • GET /api/v1/auth/my-permissions — with EMPLOYE role and mixed module states
  • GET /api/v1/auth/my-permissions — with SUSPENDED tenant
  • GET /api/v1/auth/my-permissions — user with no membership → 403
  • Cache behavior: second call within TTL returns cached result

What QA Will Validate

  • Permission endpoint returns correct permissions for various role/module combinations
  • Suspended tenant correctly strips write permissions
  • Inactive module permissions are silently excluded

Out of Scope

  • Custom roles — V2. MVP has fixed 5 default roles only.
  • Permission UI (viewing/editing permissions in admin) — V2
  • Module activation/deactivation — CP-007 (stubbed here)
  • Tenant switching — CP-011

Definition of Done

  • [ ] PermissionService resolves permissions correctly with module gating
  • [ ] PermissionCacheService caches and invalidates correctly
  • [ ] GET /api/v1/auth/my-permissions endpoint returns correct data
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Module gating stubbed (all modules ACTIVE) until CP-007
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format