Security: enforce per-user client restrictions on client, financial and bulk handlers and per-client reports

Agents restricted from a client could still edit, archive, restore or delete it, run bulk actions on it (edits, tags, archive, email, tickets) and add credits. Every selected client is now checked before anything is written.

Expense, trip and recurring expense edits now check the client the record already belongs to, not just the submitted one. Recurring expense delete, revenue edit/delete and task approval handlers gained the same check.

Per-client reports and the dashboard payment/ticket lists now hide clients the agent is denied. Company-wide totals are unchanged.
This commit is contained in:
johnnyq
2026-10-08 16:17:46 -04:00
parent 5acb1802bb
commit cd7ebc0a5f
15 changed files with 111 additions and 7 deletions

View File

@@ -123,6 +123,7 @@ if ($user_config_dashboard_financial_enable == 1) {
SELECT client_name, invoice_number, invoice_prefix, payment_amount, payment_date FROM payments
JOIN invoices ON payment_invoice_id = invoice_id
JOIN clients ON invoice_client_id = client_id
WHERE 1=1 " . clientScopeSql('invoice_client_id') . "
ORDER BY payment_id DESC LIMIT 5
");
@@ -607,6 +608,7 @@ if ($user_config_dashboard_technical_enable == 1) {
LEFT JOIN contacts ON ticket_contact_id = contact_id
WHERE ticket_assigned_to = $session_user_id
AND ticket_closed_at IS NULL
" . clientScopeSql('ticket_client_id') . "
ORDER BY ticket_number DESC
");
?>

View File

@@ -140,6 +140,8 @@ if (isset($_POST['edit_client'])) {
$client_id = intval($_POST['client_id']);
enforceClientAccess();
// Update client
mysqli_query($mysqli, "UPDATE clients SET client_name = '$name', client_type = '$type', client_website = '$website', client_referral = '$referral', client_rate = $rate, client_net_terms = $net_terms, client_tax_id_number = '$tax_id_number', client_lead = $lead, client_abbreviation = '$abbreviation', client_notes = '$notes' WHERE client_id = $client_id");
@@ -212,6 +214,8 @@ if (isset($_GET['archive_client'])) {
$client_id = intval($_GET['archive_client']);
enforceClientAccess();
// Archive client
mysqli_query($mysqli, "UPDATE clients SET client_archived_at = NOW() WHERE client_id = $client_id");
@@ -242,6 +246,8 @@ if (isset($_GET['restore_client'])) {
$client_id = intval($_GET['restore_client']);
enforceClientAccess();
// Get Client Name
$client_name = escapeSql(getFieldById('clients', $client_id, 'client_name'));
@@ -263,6 +269,8 @@ if (isset($_GET['delete_client'])) {
$client_id = intval($_GET['delete_client']);
enforceClientAccess();
// Get Client Name
$client_name = escapeSql(getFieldById('clients', $client_id, 'client_name'));
@@ -761,6 +769,11 @@ if (isset($_POST['bulk_add_client_ticket'])) {
// Create ticket for each selected asset
if (isset($_POST['client_ids'])) {
// Check every selected client before creating any tickets
foreach ($_POST['client_ids'] as $client_id) {
enforceClientAccess(intval($client_id));
}
// Get a Asset Count
$client_count = count($_POST['client_ids']);
@@ -844,6 +857,11 @@ if (isset($_POST['bulk_edit_client_industry'])) {
if (isset($_POST['client_ids'])) {
// Check every selected client before changing any of them
foreach ($_POST['client_ids'] as $client_id) {
enforceClientAccess(intval($client_id));
}
$count = count($_POST['client_ids']);
foreach($_POST['client_ids'] as $client_id) {
@@ -878,6 +896,11 @@ if (isset($_POST['bulk_edit_client_referral'])) {
if (isset($_POST['client_ids'])) {
// Check every selected client before changing any of them
foreach ($_POST['client_ids'] as $client_id) {
enforceClientAccess(intval($client_id));
}
$count = count($_POST['client_ids']);
foreach($_POST['client_ids'] as $client_id) {
@@ -912,6 +935,11 @@ if (isset($_POST['bulk_edit_client_hourly_rate'])) {
if (isset($_POST['client_ids'])) {
// Check every selected client before changing any of them
foreach ($_POST['client_ids'] as $client_id) {
enforceClientAccess(intval($client_id));
}
$count = count($_POST['client_ids']);
foreach($_POST['client_ids'] as $client_id) {
@@ -946,6 +974,11 @@ if (isset($_POST['bulk_edit_client_net_terms'])) {
if (isset($_POST['client_ids'])) {
// Check every selected client before changing any of them
foreach ($_POST['client_ids'] as $client_id) {
enforceClientAccess(intval($client_id));
}
$count = count($_POST['client_ids']);
foreach($_POST['client_ids'] as $client_id) {
@@ -978,6 +1011,11 @@ if (isset($_POST['bulk_assign_client_tags'])) {
if (isset($_POST['client_ids'])) {
// Check every selected client before changing any of them
foreach ($_POST['client_ids'] as $client_id) {
enforceClientAccess(intval($client_id));
}
$count = count($_POST['client_ids']);
foreach($_POST['client_ids'] as $client_id) {
@@ -1024,6 +1062,11 @@ if (isset($_POST['bulk_send_client_email']) && isset($_POST['client_ids'])) {
$client_ids = array_map('intval', $_POST['client_ids']);
$count = count($client_ids);
// Check every selected client before queuing anything
foreach ($client_ids as $client_id) {
enforceClientAccess($client_id);
}
// Email metadata
$mail_from = escapeSql($_POST['mail_from']);
$mail_from_name = escapeSql($_POST['mail_from_name']);
@@ -1112,6 +1155,11 @@ if (isset($_POST['bulk_archive_clients'])) {
if (isset($_POST['client_ids'])) {
// Check every selected client before changing any of them
foreach ($_POST['client_ids'] as $client_id) {
enforceClientAccess(intval($client_id));
}
$count = 0;
foreach ($_POST['client_ids'] as $client_id) {
@@ -1148,6 +1196,11 @@ if (isset($_POST['bulk_unarchive_clients'])) {
if (isset($_POST['client_ids'])) {
// Check every selected client before changing any of them
foreach ($_POST['client_ids'] as $client_id) {
enforceClientAccess(intval($client_id));
}
$count = count($_POST['client_ids']);
foreach ($_POST['client_ids'] as $client_id) {

View File

@@ -13,6 +13,9 @@ if (isset($_POST['add_credit'])) {
enforceUserPermission('module_sales', 2);
$client_id = intval($_POST['client']);
enforceClientAccess();
$amount = floatval($_POST['amount']);
$type = escapeSql($_POST['type']);
$expire = escapeSql($_POST['expire']);

View File

@@ -57,12 +57,18 @@ if (isset($_POST['edit_expense'])) {
require_once 'expense_model.php';
$expense_id = intval($_POST['expense_id']);
// Check the client the expense belongs to now, not just the one submitted
$existing_client_id = intval(getFieldById('expenses', $expense_id, 'expense_client_id'));
if ($existing_client_id) {
enforceClientAccess($existing_client_id);
}
if ($client_id) {
enforceClientAccess();
}
$expense_id = intval($_POST['expense_id']);
// Get old receipt
$existing_file_name = escapeSql(getFieldById('expenses', $expense_id, 'expense_receipt'));

View File

@@ -63,6 +63,12 @@ if (isset($_POST['edit_recurring_expense'])) {
$description = escapeSql($_POST['description']);
$reference = escapeSql($_POST['reference']);
// Check the client the recurring expense belongs to now, not just the one submitted
$existing_client_id = intval(getFieldById('recurring_expenses', $recurring_expense_id, 'recurring_expense_client_id'));
if ($existing_client_id) {
enforceClientAccess($existing_client_id);
}
if ($client_id) {
enforceClientAccess();
}
@@ -97,6 +103,10 @@ if (isset($_GET['delete_recurring_expense'])) {
$recurring_expense_description = escapeSql($row['recurring_expense_description']);
$client_id = intval($row['recurring_expense_client_id']);
if ($client_id) {
enforceClientAccess();
}
mysqli_query($mysqli,"DELETE FROM recurring_expenses WHERE recurring_expense_id = $recurring_expense_id");
logAudit("Recurring Expense", "Delete", "$session_name deleted recurring expense $recurring_expense_description", $client_id);

View File

@@ -39,6 +39,12 @@ if (isset($_POST['edit_revenue'])) {
enforceUserPermission('module_sales', 2);
$revenue_id = intval($_POST['revenue_id']);
$client_id = intval(getFieldById('revenues', $revenue_id, 'revenue_client_id'));
if ($client_id) {
enforceClientAccess();
}
$date = escapeSql($_POST['date']);
$amount = floatval($_POST['amount']);
$account = intval($_POST['account']);
@@ -65,6 +71,11 @@ if (isset($_GET['delete_revenue'])) {
$revenue_id = intval($_GET['delete_revenue']);
$client_id = intval(getFieldById('revenues', $revenue_id, 'revenue_client_id'));
if ($client_id) {
enforceClientAccess();
}
// Get Revenue Details
$revenue_description = escapeSql(getFieldById('revenues', $revenue_id, 'revenue_description'));

View File

@@ -364,6 +364,9 @@ if (isset($_GET['approve_ticket_task'])) {
exit;
}
$client_id = intval(getFieldById('tickets', $ticket_id, 'ticket_client_id'));
enforceClientAccess();
// Validate approver (deny)
if ($required_user > 0 && $required_user !== $session_user_id) {
flashAlert("You cannot approve that task", 'error');
@@ -399,6 +402,11 @@ if (isset($_GET['delete_ticket_task_approver'])) {
$approval_id = intval($_GET['delete_ticket_task_approver']);
$sql = mysqli_query($mysqli, "SELECT ticket_client_id FROM task_approvals LEFT JOIN tasks ON task_id = approval_task_id LEFT JOIN tickets ON ticket_id = task_ticket_id WHERE approval_id = $approval_id");
$row = mysqli_fetch_assoc($sql);
$client_id = intval($row['ticket_client_id'] ?? 0);
enforceClientAccess();
mysqli_query($mysqli, "DELETE FROM task_approvals WHERE approval_id = $approval_id");
logAudit("Task", "Delete", "$session_name deleted task approval request ($approval_id)", 0, 0);

View File

@@ -40,6 +40,12 @@ if (isset($_POST['edit_trip'])) {
$trip_id = intval($_POST['trip_id']);
// Check the client the trip belongs to now, not just the one submitted
$existing_client_id = intval(getFieldById('trips', $trip_id, 'trip_client_id'));
if ($existing_client_id) {
enforceClientAccess($existing_client_id);
}
$client_id = intval($_POST['client_id']);
if ($client_id) {

View File

@@ -76,6 +76,7 @@ $billing_increment_minutes = (int) round($billing_increment_seconds / 60);
// Ticket-level billable flag (same as your original report)
$billable_sql = $billable_only ? " AND t.ticket_billable = 1 " : "";
$client_scope_sql = clientScopeSql('c.client_id');
/**
* Query returns ONLY replies that have time_worked and are within date range.
@@ -108,6 +109,7 @@ $stmt = $mysqli->prepare("
WHERE c.client_archived_at IS NULL
$billable_sql
$client_scope_sql
ORDER BY c.client_name ASC,
t.ticket_number ASC,

View File

@@ -48,9 +48,10 @@ $sql_payment_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(payment_date) A
$sql_clients = "SELECT c.client_id, c.client_name, SUM(p.payment_amount) AS amount_paid
FROM clients AS c
JOIN invoices AS i ON c.client_id = i.invoice_client_id
JOIN payments AS p ON i.invoice_id = p.payment_invoice_id";
JOIN payments AS p ON i.invoice_id = p.payment_invoice_id
WHERE 1=1" . clientScopeSql('c.client_id');
if ($year != 'all') {
$sql_clients .= " WHERE YEAR(p.payment_date) = $year";
$sql_clients .= " AND YEAR(p.payment_date) = $year";
}
$sql_clients .= " GROUP BY c.client_id
HAVING amount_paid > 599

View File

@@ -38,6 +38,7 @@ enforceUserPermission('module_financial');
GROUP BY payment_invoice_id) as payments
ON
invoices.invoice_id = payments.payment_invoice_id
WHERE 1=1 " . clientScopeSql('clients.client_id') . "
GROUP BY
clients.client_id,
clients.client_name

View File

@@ -11,6 +11,7 @@ $sql = mysqli_query($mysqli, "
FROM clients
LEFT JOIN recurring_invoices ON client_id = recurring_invoice_client_id
WHERE recurring_invoice_status = 1
" . clientScopeSql('clients.client_id') . "
GROUP BY clients.client_id
HAVING recurring_monthly_total > 0
ORDER BY recurring_monthly_total DESC

View File

@@ -25,7 +25,7 @@ if ($month) {
$sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC");
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC");
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('client_id') . " ORDER BY client_name ASC");
// Clock time spent on resolved tickets, gathered for every client at once

View File

@@ -18,7 +18,7 @@ if (isset($_GET['month'])) {
$sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC");
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL ORDER BY client_name ASC");
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE client_archived_at IS NULL " . clientScopeSql('client_id') . " ORDER BY client_name ASC");
?>

View File

@@ -12,7 +12,7 @@ if (isset($_GET['year'])) {
$sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC");
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients ORDER BY client_name ASC");
$sql_clients = mysqli_query($mysqli, "SELECT client_id, client_name FROM clients WHERE 1=1 " . clientScopeSql('client_id') . " ORDER BY client_name ASC");
$rows = 0;