Skip to content

Phase 7 HR — Full Security Audit

FieldValue
Date2026-08-02
Re-verified2026-08-02 (post-CI green + residual harden)
ScopeEmployees · Leave Management · Attendance · Payroll (API + tenant SPA + docs)
Branchfeature/phase-7-hr-9630
StatusRemediation applied — production-ready for opt-in Marketplace enablement
CompanionProduction Readiness
ArchitecturePlatform freeze respected — thin modules on existing foundation

Executive verdict

Phase 7 HR is cleared for production as four free Marketplace SKUs under category hr, after security audit, remediation, CI green on companion PRs, and a second verification pass.

DimensionRatingNotes
Security (authz / tenancy / IDOR)Passmodule:* + can:* + BelongsToTenant; isolation tests green
Data integrity (leave / payroll)PassDays/range, balance locks, pay-run locks, journal account types, force-delete retention
Compensation privacyPassDefault staff no longer has payroll.view
Frontend gates & XSSPassRequireAccess / PermissionGate; notes as React text
Automated testsPassPest HR suites + headed Playwright per module
CI (companion PRs)PassBackend #72 · Frontend #66 · Docs #75 Quality Gates green; Website #12 mergeable
Docs / opsPassUser · developer · deployment · API · this report + readiness pack
Residual riskLow / acceptedSoft-delete uniques, SoD, sort whitelist, existing-tenant staff re-sync — non-blocking

Go / no-go: GO for merge and migrate-only rollout. Complete the production readiness checklist on staging before production traffic.


Delivery matrix

SKUSlugHard depBillableUIPestPlaywright
EmployeesemployeesFree opt-inHR navYestest:e2e:employees (6/6 headed)
Leave Managementleave-managementemployeesFree opt-inHR navYestest:e2e:leave-management (6/6 headed)
AttendanceattendanceemployeesFree opt-inHR navYestest:e2e:attendance (6/6 headed)
Payrollpayrollemployees (+ optional Accounting)Free opt-inHR navYestest:e2e:payroll (6/6 headed)

Companion packs: Employees · Leave · Attendance · Payroll.


What was audited

Backend

  • Routes: auth:tenant-api + module:{slug} + can:{permission} (including approve / pay / post / force.delete)
  • Policies + Gate::authorize in controllers
  • Default role map (config/tenant-default-role-permissions.php)
  • Form requests — no client write of status, reviewed_by, tenant_id, journal_entry_id
  • Services: leave lifecycle, balances, attendance uniqueness, pay-run lines, soft journal post
  • Soft-delete / restore / force-delete authorization (approved leave retained on soft and force paths)
  • Tenant isolation (BelongsToTenant / TenantScope) and cross-workspace Pest cases

Frontend

  • HR nav gated by module entitlement + permission
  • Route RequireAccess and action PermissionGate (approve / pay / post / delete)
  • Leave / attendance / payroll free-text fields rendered as React children (no dangerouslySetInnerHTML)
  • Headed Playwright: validation errors, CRUD, leave submit→approve, payroll approve→paid

Ops / docs

  • Migrate-only catalog registration (DefaultModuleRegistrar)
  • Module dependency rows (Leave/Attendance/Payroll → Employees; Payroll soft → Accounting)
  • User / developer / deployment / API guides for all four SKUs

Findings summary

IDSeverityAreaStatus
HR-01MediumLeave balance upsert used create to overwrite existing rowsFixed
HR-02MediumApproved leave requests could be deleted without reversing usedFixed
HR-03MediumLeave days could exceed date range; approve ignored remainingFixed
HR-04MediumConcurrent leave approve / reject lacked balance / request lockingFixed
HR-05MediumPay-run line gross unbounded / negative; approve lacked locksFixed
HR-06MediumJournal post accepted any account type; duplicate-post raceFixed
HR-07MediumDefault staff role included payroll.view (full salary exposure)Fixed
HR-08LowEmployee user_id could link one user to many employeesFixed (app validation)
HR-09LowAttendance allowed check_out before check_inFixed
HR-10LowClient-supplied leave remaining trusted on upsert / updateFixed (always derived)
HR-11InfoNo segregation-of-duties on approve (same actor can create + approve)Accepted (SMB default)
HR-12InfoUnvalidated list sort/direction (platform-wide pattern)Accepted / follow-up
HR-13InfoSoft-delete unique indexes include trashed rowsFollow-up
HR-14InfoPrivileged attrs remain $fillable but not request-writableAccepted (defense-in-depth follow-up)
HR-15MediumforceDelete on leave requests skipped status retention guardFixed (re-audit)
HR-16MediumExisting tenants may retain staff payroll.view (additive sync)Ops residual
HR-17LowEmployee user_id uniqueness is validation-only (no DB unique)Follow-up

No Critical or High open findings remain on intended new-tenant / soft-delete paths.


Remediation details

Leave Management

  • Upsert authorizes update when a balance already exists; create path keeps create.
  • Upsert and update always derive remaining = entitled - used (client remaining ignored).
  • Leave days capped to inclusive calendar span (FormRequest + service).
  • Approve runs in a transaction, locks the request, locks the balance, and rejects insufficient remaining.
  • Reject / submit / cancel also lock the request row.
  • Soft-delete and force-delete allowed only for draft, rejected, or cancelled (approved retained for audit).
  • Optional null days from the UI auto-calculates inclusive calendar days.

Payroll

  • Line gross validated min:0; adjustments bounded; net cannot go negative on update.
  • Approve / mark paid / post wrapped in lockForUpdate() transactions.
  • Approve rejects negative line amounts.
  • Journal post requires active expense debit and liability credit accounts (request + service).

RBAC

  • Default staff role no longer includes payroll.view. Manager+ retains compensation access.
  • Ops note: Existing workspaces keep previously synced role permissions until roles are re-synced or edited. New tenants get the tightened defaults.

Employees / Attendance

  • user_id unique per tenant among non-deleted employees (FormRequest + Pest).
  • check_out must be on or after check_in when both are present.

Security checklist

CheckResult
Module catalog + hard deps (Leave/Attendance/Payroll → Employees)Pass
Permission middleware on mutating + sensitive routesPass
Tenant scoping / isolation testsPass
Leave lifecycle integrity (days, balance, soft + force delete)Pass
Payroll lifecycle integrity (amounts, locks, journal types)Pass
Staff cannot read salaries by defaultPass (new tenants)
Soft-delete / restore authorization presentPass
Frontend RequireAccess / PermissionGate on HR routes & actionsPass
XSS: leave/attendance/payroll notes as React text nodesPass
Platform freeze (no parallel auth/tenancy/billing)Pass
Pest Leave / Payroll / Employee / Attendance (security regressions)48+ passed (incl. approved force-delete retention)
Headed Playwright Employees / Leave / Attendance / Payroll6/6 each (2026-08-02)
Companion PR CIGreen (Backend QG + Pest; Frontend QG; Docs QG)

Ops deploy / smoke / rollback: see Production Readiness.


Default RBAC after this ship

RoleEmployeesLeaveAttendancePayroll
Owner / AdminFullFullFullFull (incl. approve / pay / post)
ManagerView + mutate (per map)View + create/update + approveView + mutateView + create/update + approve/pay/post
StaffViewViewViewNo payroll.view

Exact permission names: see Tenant RBAC and module developer guides.


Residual / follow-up (non-blocking)

  1. Partial unique indexes for soft-deleted uniques (leave_balances, attendance_records, payroll_profiles, employees.user_id) to avoid recreate / race collisions.
  2. Whitelist sort/direction across tenant list services (platform-wide hardening).
  3. Optional SoD: reject leave/pay approve when actor is creator (config flag).
  4. FK + index on pay_runs.journal_entry_id.
  5. Remove privileged attributes from model $fillable and use explicit forceFill in services.
  6. Overlap validation for pending/approved leave ranges per employee.
  7. Re-sync default role permissions for existing tenants that already received payroll.view on staff.

Test evidence

bash
# Backend
cd EloSync-Backend
php artisan test --compact \
  tests/Feature/Tenant/Leave/LeaveManagementTest.php \
  tests/Feature/Tenant/Payroll/PayrollTest.php \
  tests/Feature/Tenant/Employee/EmployeeTest.php \
  tests/Feature/Tenant/Attendance/AttendanceTest.php

# Frontend headed (per module)
cd EloSync-Frontend
npm run test:e2e:employees:headed
npm run test:e2e:leave-management:headed
npm run test:e2e:attendance:headed
npm run test:e2e:payroll:headed

Security coverage: inflated leave days, insufficient balance on approve, soft + force delete of approved leave, balance upsert RBAC, derived remaining, negative pay-run gross, wrong journal account types, staff payroll.view denial, unique employee user_id, attendance check-out ordering.


RepoPRBranchCI (re-check)
EloSync-Backend#72feature/phase-7-hr-9630Quality Gate ✅ · Pest ✅ · CLEAN (draft)
EloSync-Frontend#66feature/phase-7-hr-9630Quality Gate ✅ · CLEAN (draft)
EloSync-Docs#75feature/phase-7-hr-9630Quality Gate ✅ · CLEAN (draft)
elosync-website#12feature/phase-7-hr-9630CLEAN (draft; no required checks)

Sign-off

RoleDecision
Security auditRemediation complete — no open Critical/High
Production readinessComplete ops checklist in Production Readiness
Recommended next stepMark draft PRs ready → merge Backend → Frontend → Docs → Website → migrate staging → smoke → production migrate

Official documentation for the EloSync SaaS Platform.