feat(biometrics): enforce strict branch scoping on biometric attendance and sync

- Restrict biometric attendances query and grouped records strictly to employees of assigned branch
- Scope bulk sync (syncAll) to only sync biometric records for employees within the user's branch
- Guard individual sync and custom sync against processing employees from foreign branches
- Tighten attendance records query to strictly filter by employee assigned branch
- Add feature test verifying biometric attendance isolation and permission guards
This commit is contained in:
2026-09-10 11:32:48 +08:00
parent 33f776c6b2
commit ef1aa7cf24
3 changed files with 143 additions and 17 deletions

View File

@@ -169,20 +169,22 @@ class BiometricAttendanceController extends Controller
// Filter by permissions
if ($scopedBranchId) {
$branchEmpCodes = \App\Models\Employee::where('branch_id', $scopedBranchId)->whereNotNull('biometric_emp_id')->pluck('biometric_emp_id');
$query->where(function($q) use ($scopedBranchId, $branchEmpCodes) {
$q->where('branch_id', $scopedBranchId)
->orWhereIn('biometric_emp_id', $branchEmpCodes);
});
if ($branchEmpCodes->isEmpty()) {
$query->whereRaw('1 = 0');
} else {
$query->whereIn('biometric_emp_id', $branchEmpCodes);
}
} elseif ($canManageAny) {
// Full company access
} elseif ($canManageBranch) {
$branchId = $user->branch_id ?? $user->employee?->branch_id;
if ($branchId) {
$branchEmpCodes = \App\Models\Employee::where('branch_id', $branchId)->whereNotNull('biometric_emp_id')->pluck('biometric_emp_id');
$query->where(function($q) use ($branchId, $branchEmpCodes) {
$q->where('branch_id', $branchId)
->orWhereIn('biometric_emp_id', $branchEmpCodes);
});
if ($branchEmpCodes->isEmpty()) {
$query->whereRaw('1 = 0');
} else {
$query->whereIn('biometric_emp_id', $branchEmpCodes);
}
} else {
// Manager without a specific branch assigned has company-wide management
}
@@ -266,6 +268,13 @@ class BiometricAttendanceController extends Controller
})
->values();
if ($scopedBranchId) {
$groupedAttendances = $groupedAttendances->filter(function ($item) use ($employees, $scopedBranchId) {
$emp = $employees->get($item['employee_code']);
return $emp && (int)$emp->branch_id === (int)$scopedBranchId;
})->values();
}
// Attach employee names
$groupedAttendances = $groupedAttendances->map(function($item) use ($employees) {
$emp = $employees->get($item['employee_code']);
@@ -437,7 +446,19 @@ class BiometricAttendanceController extends Controller
$overwrite = $request->boolean('overwrite', false);
$scopedBranchId = authBranchId();
$query = \App\Models\BiometricAttendance::query();
if ($scopedBranchId) {
$branchEmpCodes = Employee::where('branch_id', $scopedBranchId)
->whereNotNull('biometric_emp_id')
->pluck('biometric_emp_id');
if ($branchEmpCodes->isEmpty()) {
$query->whereRaw('1 = 0');
} else {
$query->whereIn('biometric_emp_id', $branchEmpCodes);
}
}
if (!$overwrite) {
$query->where('sync_status', 'pending');
@@ -456,11 +477,15 @@ class BiometricAttendanceController extends Controller
// Eager-load employees with shifts
$empIds = $pendingRecords->pluck('biometric_emp_id')->unique();
$employees = Employee::with(['user', 'shift'])
$empQuery = Employee::with(['user', 'shift'])
->whereIn('created_by', getCompanyAndUsersId())
->whereIn('biometric_emp_id', $empIds)
->get()
->keyBy('biometric_emp_id');
->whereIn('biometric_emp_id', $empIds);
if ($scopedBranchId) {
$empQuery->where('branch_id', $scopedBranchId);
}
$employees = $empQuery->get()->keyBy('biometric_emp_id');
$groupedAttendances = $pendingRecords->groupBy(function ($item) use ($employees) {
$employee = $employees->get($item->biometric_emp_id);
@@ -591,6 +616,11 @@ class BiometricAttendanceController extends Controller
$employee = Employee::with(['user', 'shift', 'branch'])->whereIn('created_by', getCompanyAndUsersId())->where('biometric_emp_id', $biometricEmpId)->first();
$scopedBranchId = authBranchId();
if ($scopedBranchId && $employee && (int)$employee->branch_id !== (int)$scopedBranchId) {
return redirect()->back()->with('error', __('Permission Denied. Employee belongs to another branch.'));
}
if ($employee) {
// Check if record already exists
$exists = AttendanceRecord::where('employee_id', $employee->user_id)
@@ -690,6 +720,11 @@ class BiometricAttendanceController extends Controller
return redirect()->back()->with('error', __('Employee not found.'));
}
$scopedBranchId = authBranchId();
if ($scopedBranchId && (int)$employee->branch_id !== (int)$scopedBranchId) {
return redirect()->back()->with('error', __('Permission Denied. Employee belongs to another branch.'));
}
$shift = Shift::where('id', $employee->shift_id)->where('status', 'active')->first() ?? Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first();
// Check if record already exists