Files
itflow/agent/post/asset_interface.php
johnnyq 359851c876 Security sweep of the 26.11 work: API contact lookup by phone or mobile kept to the key's clients; API credentials refuse a wrong decrypt password instead of saving unreadable usernames and passwords, link credential_vendor_id and credential_software_id, and drop another client's contact or asset; Products Receive window lists expenses only with Financial access and only for clients the agent can open; account ledger, reconcile page and ledger export show lines for clients the agent can't open as Another client with only date, kind and amount, with search and the client filter kept to reachable clients; Tax Collected folds unreachable exempt clients into one line; client-limited agents no longer see other clients' names in the export window client filters, Transfer to Client, company vendor expense lists, My Activity or the New Client duplicate check; Stripe switched off stops guest pay links and saving cards, and the Stripe return never books a payment twice; Admin > Cron job window is admins only; records per page needs the CSRF token and takes only the listed sizes; uploads/canned_responses closed to direct access; Admin > Debug no longer stops at the footer; report export audit log no longer double-escapes names; bulk interface network refuses a deleted network; API assets/update keeps the primary interface IP, MAC and network, tickets/resolve reports success, software/read by name works, clients/create takes the company currency, certificates/create returns its error, clients/archive logs as Client, and the 405 response no longer echoes the method; changelog database step count corrected to sixty
2026-10-23 12:09:07 -04:00

483 lines
18 KiB
PHP

<?php
/*
* ITFlow - GET/POST request handler for asset interfaces
*
* The ports and network cards of an asset (functions/asset_interface.php):
* checked by interfaceFormSet() - an address inside its network, a MAC that is
* one, a network of the asset's client - and connected one-to-one by
* interfaceConnect(). The asset's own form keeps its primary interface in step
* (agent/post/asset.php).
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
/** What saving an interface says - its name, and anything worth knowing (the address is on another device too). */
function interfaceSavedMessage($verb, $name, $warnings) {
if ($warnings) {
flashAlert("Interface <strong>$name</strong> $verb - " . implode('; ', $warnings) . '.', 'warning');
} else {
flashAlert("Interface <strong>$name</strong> $verb");
}
}
if (isset($_POST['add_asset_interface'])) {
validateCSRFToken();
$asset = interfaceAssetForRequest($_POST['asset_id'] ?? 0, 2);
$asset_id = intval($asset['asset_id']);
$error = '';
$warnings = [];
$set = interfaceFormSet($_POST, $asset, 0, $error, $warnings);
if ($set === '') {
flashAlert($error, 'error');
redirect();
}
mysqli_query($mysqli, "INSERT INTO asset_interfaces SET $set, interface_asset_id = $asset_id");
$interface_id = mysqli_insert_id($mysqli);
// The first one is the primary whether it is ticked or not
$has_primary = mysqli_num_rows(mysqli_query($mysqli, "SELECT 1 FROM asset_interfaces WHERE interface_asset_id = $asset_id AND interface_primary = 1 AND interface_archived_at IS NULL"));
if (!empty($_POST['primary_interface']) || !$has_primary) {
interfaceMakePrimary($interface_id, $asset_id);
}
$connect_to = interfaceConnectTarget($_POST['connected_to'] ?? 0, $client_id, $asset_id);
if ($connect_to) {
interfaceConnect($interface_id, $connect_to);
}
$name = trim($_POST['name']);
logAudit("Asset Interface", "Create", "$session_name created interface " . escapeSql($name) . " on asset " . escapeSql($asset['asset_name']), $client_id, $asset_id);
interfaceSavedMessage('created', $name, $warnings);
redirect();
}
if (isset($_POST['edit_asset_interface'])) {
validateCSRFToken();
$row = interfaceForRequest($_POST['interface_id'] ?? 0, 2);
$interface_id = intval($row['interface_id']);
$asset_id = intval($row['asset_id']);
$error = '';
$warnings = [];
$set = interfaceFormSet($_POST, $row, $interface_id, $error, $warnings);
if ($set === '') {
flashAlert($error, 'error');
redirect();
}
mysqli_query($mysqli, "UPDATE asset_interfaces SET $set WHERE interface_id = $interface_id");
if (!empty($_POST['primary_interface'])) {
interfaceMakePrimary($interface_id, $asset_id);
}
// What it is plugged into - moved, or let go when emptied
if (isset($_POST['connected_to'])) {
$connect_to = interfaceConnectTarget($_POST['connected_to'], $client_id, $asset_id);
$current = intval($row['link']['interface_id'] ?? 0);
if ($connect_to !== $current) {
if ($current) {
interfaceConnect($interface_id, 0);
}
if ($connect_to) {
interfaceConnect($interface_id, $connect_to);
}
}
}
$name = trim($_POST['name']);
logAudit("Asset Interface", "Edit", "$session_name edited interface " . escapeSql($name) . " on asset " . escapeSql($row['asset_name']), $client_id, $asset_id);
interfaceSavedMessage('saved', $name, $warnings);
redirect();
}
if (isset($_GET['disconnect_asset_interface'])) {
validateCSRFToken();
$row = interfaceForRequest($_GET['disconnect_asset_interface'], 2);
if ($row['link']) {
interfaceConnect($row['interface_id'], 0);
logAudit("Asset Interface", "Edit", "$session_name disconnected interface " . escapeSql($row['interface_name']) . " on asset " . escapeSql($row['asset_name']) . " from " . escapeSql($row['link']['asset_name'] . ' ' . $row['link']['interface_name']), $client_id, intval($row['asset_id']));
}
flashAlert("<strong>" . $row['asset_name'] . ' ' . $row['interface_name'] . "</strong> disconnected");
redirect();
}
if (isset($_GET['primary_asset_interface'])) {
validateCSRFToken();
$row = interfaceForRequest($_GET['primary_asset_interface'], 2);
interfaceMakePrimary($row['interface_id'], $row['asset_id']);
logAudit("Asset Interface", "Edit", "$session_name made interface " . escapeSql($row['interface_name']) . " the primary of asset " . escapeSql($row['asset_name']), $client_id, intval($row['asset_id']));
flashAlert("<strong>" . $row['interface_name'] . "</strong> is the primary interface of " . $row['asset_name']);
redirect();
}
if (isset($_GET['delete_asset_interface'])) {
validateCSRFToken();
$row = interfaceForRequest($_GET['delete_asset_interface'], 2);
// Its connection goes with it (ON DELETE CASCADE)
mysqli_query($mysqli, "DELETE FROM asset_interfaces WHERE interface_id = " . intval($row['interface_id']));
logAudit("Asset Interface", "Delete", "$session_name deleted interface " . escapeSql($row['interface_name']) . " from asset " . escapeSql($row['asset_name']), $client_id, intval($row['asset_id']));
flashAlert("Interface <strong>" . $row['interface_name'] . "</strong> deleted", 'error');
redirect();
}
// A run of ports - "Gi1/0/" 1 to 24, "eth" 0 to 3
if (isset($_POST['add_asset_multiple_interfaces'])) {
validateCSRFToken();
$asset = interfaceAssetForRequest($_POST['asset_id'] ?? 0, 2);
$asset_id = intval($asset['asset_id']);
$count = intval($_POST['interfaces'] ?? 0);
if ($count < 1 || $count > 128) {
flashAlert('Add from 1 to 128 interfaces at a time.', 'error');
redirect();
}
$names = interfaceSeriesNames(trim((string) ($_POST['name_prefix'] ?? '')), $_POST['interface_start'] ?? 1, $count, $_POST['pad'] ?? 0);
$type = escapeSql(mb_substr(trim((string) ($_POST['type'] ?? '')), 0, 50));
$network_id = intval(assetClientOwnedId('network', $_POST['network_id'] ?? 0, $client_id));
$vlans = escapeSql(interfaceVlansText($_POST['vlans'] ?? ''));
$notes = escapeSql(trim((string) ($_POST['notes'] ?? '')));
$existing = [];
$sql = mysqli_query($mysqli, "SELECT interface_name FROM asset_interfaces WHERE interface_asset_id = $asset_id AND interface_archived_at IS NULL");
while ($row = mysqli_fetch_row($sql)) {
$existing[mb_strtolower($row[0])] = true;
}
$added = 0;
$skipped = 0;
foreach ($names as $name) {
if (isset($existing[mb_strtolower($name)])) {
$skipped++;
continue;
}
mysqli_query($mysqli, "INSERT INTO asset_interfaces SET interface_name = '" . escapeSql(mb_substr($name, 0, 200)) . "', interface_type = '$type',
interface_vlans = '$vlans', interface_notes = '$notes', interface_network_id = $network_id, interface_asset_id = $asset_id");
$added++;
}
logAudit("Asset Interface", "Bulk Create", "$session_name created $added interface(s) " . escapeSql($names[0] . ' - ' . end($names)) . " on asset " . escapeSql($asset['asset_name']), $client_id, $asset_id);
flashAlert("Added <strong>$added</strong> interface" . ($added === 1 ? '' : 's') . " to <strong>" . $asset['asset_name'] . "</strong>" . ($skipped ? " - $skipped already there" : ''));
redirect();
}
if (isset($_POST['bulk_edit_asset_interface_type'])) {
validateCSRFToken();
$type = mb_substr(trim((string) ($_POST['bulk_type'] ?? '')), 0, 50);
$count = 0;
foreach (interfacesForBulk(2) as $interface_id => $row) {
mysqli_query($mysqli, "UPDATE asset_interfaces SET interface_type = '" . escapeSql($type) . "' WHERE interface_id = $interface_id");
logAudit("Asset Interface", "Edit", "$session_name set the type of interface " . escapeSql($row['interface_name']) . " on asset " . escapeSql($row['asset_name']) . " to " . escapeSql($type ?: 'none'), intval($row['asset_client_id']), intval($row['asset_id']));
$count++;
}
flashAlert("Type set to <strong>" . ($type ?: 'none') . "</strong> on <strong>$count</strong> interface" . ($count === 1 ? '' : 's'));
redirect();
}
if (isset($_POST['bulk_edit_asset_interface_network'])) {
validateCSRFToken();
$network_id = intval($_POST['bulk_network'] ?? 0);
$network = $network_id ? networkRow($network_id) : null;
if ($network_id && !$network) {
flashAlert('That network no longer exists.', 'error');
redirect();
}
$count = 0;
$outside = 0;
foreach (interfacesForBulk(2) as $interface_id => $row) {
// The network must be the interface's client's
if ($network && intval($network['network_client_id']) !== intval($row['asset_client_id'])) {
continue;
}
mysqli_query($mysqli, "UPDATE asset_interfaces SET interface_network_id = $network_id WHERE interface_id = $interface_id");
$ip = normalizeIpAddress((string) $row['interface_ip']);
if ($network && $ip !== false && !isIpInSubnet($ip, $network['network'])) {
$outside++;
}
logAudit("Asset Interface", "Edit", "$session_name set the network of interface " . escapeSql($row['interface_name']) . " on asset " . escapeSql($row['asset_name']) . " to " . escapeSql($network['network_name'] ?? 'none'), intval($row['asset_client_id']), intval($row['asset_id']));
$count++;
}
flashAlert("Network set to <strong>" . ($network['network_name'] ?? 'none') . "</strong> on <strong>$count</strong> interface" . ($count === 1 ? '' : 's')
. ($outside ? " - $outside ha" . ($outside === 1 ? 's an address' : 've addresses') . " outside " . $network['network'] : ''), $outside ? 'warning' : 'success');
redirect();
}
if (isset($_POST['bulk_edit_asset_interface_ip_dhcp'])) {
validateCSRFToken();
$count = 0;
foreach (interfacesForBulk(2) as $interface_id => $row) {
mysqli_query($mysqli, "UPDATE asset_interfaces SET interface_ip = 'DHCP' WHERE interface_id = $interface_id");
logAudit("Asset Interface", "Edit", "$session_name set interface " . escapeSql($row['interface_name']) . " on asset " . escapeSql($row['asset_name']) . " to DHCP", intval($row['asset_client_id']), intval($row['asset_id']));
$count++;
}
flashAlert("Set <strong>$count</strong> interface" . ($count === 1 ? '' : 's') . " to <strong>DHCP</strong>");
redirect();
}
if (isset($_POST['bulk_edit_asset_interface_vlans'])) {
validateCSRFToken();
$vlans = interfaceVlansText($_POST['bulk_vlans'] ?? '');
$count = 0;
foreach (interfacesForBulk(2) as $interface_id => $row) {
mysqli_query($mysqli, "UPDATE asset_interfaces SET interface_vlans = '" . escapeSql($vlans) . "' WHERE interface_id = $interface_id");
logAudit("Asset Interface", "Edit", "$session_name set the VLANs of interface " . escapeSql($row['interface_name']) . " on asset " . escapeSql($row['asset_name']) . " to " . escapeSql($vlans ?: 'none'), intval($row['asset_client_id']), intval($row['asset_id']));
$count++;
}
flashAlert("VLANs set to <strong>" . ($vlans ?: 'none') . "</strong> on <strong>$count</strong> interface" . ($count === 1 ? '' : 's'));
redirect();
}
if (isset($_POST['bulk_disconnect_asset_interfaces'])) {
validateCSRFToken();
$count = 0;
foreach (interfacesForBulk(2) as $interface_id => $row) {
if ($row['link']) {
interfaceConnect($interface_id, 0);
logAudit("Asset Interface", "Edit", "$session_name disconnected interface " . escapeSql($row['interface_name']) . " on asset " . escapeSql($row['asset_name']), intval($row['asset_client_id']), intval($row['asset_id']));
$count++;
}
}
flashAlert("Disconnected <strong>$count</strong> interface" . ($count === 1 ? '' : 's'));
redirect();
}
if (isset($_POST['bulk_delete_asset_interfaces'])) {
validateCSRFToken();
$count = 0;
foreach (interfacesForBulk(2) as $interface_id => $row) {
mysqli_query($mysqli, "DELETE FROM asset_interfaces WHERE interface_id = $interface_id");
logAudit("Asset Interface", "Delete", "$session_name deleted interface " . escapeSql($row['interface_name']) . " from asset " . escapeSql($row['asset_name']), intval($row['asset_client_id']), intval($row['asset_id']));
$count++;
}
flashAlert("Deleted <strong>$count</strong> interface" . ($count === 1 ? '' : 's'), 'error');
redirect();
}
// The import's columns - an export opens straight back in, VLANs last and optional
const INTERFACE_CSV_COLUMNS = ['Name', 'Description', 'Type', 'MAC', 'IP', 'NAT IP', 'IPv6', 'Network', 'VLANs'];
if (isset($_POST['import_client_asset_interfaces_csv'])) {
validateCSRFToken();
$asset = interfaceAssetForRequest($_POST['asset_id'] ?? 0, 2);
$asset_id = intval($asset['asset_id']);
if (empty($_FILES['file']['tmp_name'])) {
flashAlert("Please select a file to upload.", 'error');
redirect();
}
if (strtolower(pathinfo((string) $_FILES['file']['name'], PATHINFO_EXTENSION)) !== 'csv') {
flashAlert("Bad file extension - only .csv files are accepted.", 'error');
redirect();
}
if ($_FILES['file']['size'] < 1) {
flashAlert("Bad file size (empty file?).", 'error');
redirect();
}
$file = fopen($_FILES['file']['tmp_name'], 'r');
$header = fgetcsv($file, 1000, ',');
if (!is_array($header) || !in_array(count($header), [8, 9], true)) {
fclose($file);
flashAlert("Bad column count - use the template: " . implode(', ', INTERFACE_CSV_COLUMNS) . " (VLANs may be left off).", 'error');
redirect();
}
// The client's networks, by name and by subnet
$networks = [];
$sql = mysqli_query($mysqli, "SELECT network_id, network_name, network FROM networks WHERE network_client_id = $client_id AND network_archived_at IS NULL");
while ($row = mysqli_fetch_assoc($sql)) {
$networks[mb_strtolower($row['network_name'])] = intval($row['network_id']);
$networks[mb_strtolower($row['network'])] = intval($row['network_id']);
}
$row_count = 0;
$duplicate_count = 0;
$problems = [];
$line = 1;
while (($column = fgetcsv($file, 1000, ',')) !== false) {
$line++;
$column = array_pad($column, 9, '');
$name = trim((string) $column[0]);
if ($name === '') {
continue;
}
$post = [
'name' => $name, 'description' => $column[1], 'type' => $column[2], 'mac' => $column[3], 'ip' => trim((string) $column[4]),
'nat_ip' => $column[5], 'ipv6' => $column[6], 'network_id' => $networks[mb_strtolower(trim((string) $column[7]))] ?? 0, 'vlans' => $column[8],
];
$error = '';
$warnings = [];
$set = interfaceFormSet($post, $asset, 0, $error, $warnings);
if ($set === '') {
if (strpos($error, 'already has an interface') !== false) {
$duplicate_count++;
} else {
$problems[] = "line $line ($name): " . strip_tags($error);
}
continue;
}
mysqli_query($mysqli, "INSERT INTO asset_interfaces SET $set, interface_asset_id = $asset_id");
$row_count++;
}
fclose($file);
logAudit("Asset Interface", "Import", "$session_name imported $row_count interface(s) to asset " . escapeSql($asset['asset_name']) . " via CSV file", $client_id, $asset_id);
$message = "<strong>$row_count</strong> interface" . ($row_count === 1 ? '' : 's') . " added to <strong>" . $asset['asset_name'] . "</strong>";
if ($duplicate_count) {
$message .= ", <strong>$duplicate_count</strong> already there";
}
if ($problems) {
$message .= ". Not imported - " . implode('; ', array_slice($problems, 0, 3)) . (count($problems) > 3 ? '; and ' . (count($problems) - 3) . ' more' : '');
}
flashAlert($message, $problems ? 'warning' : 'success');
redirect();
}
if (isset($_GET['download_client_asset_interfaces_csv_template'])) {
enforceUserPermission('module_support');
$asset_id = intval($_GET['download_client_asset_interfaces_csv_template']);
$client_id = intval(getFieldById('assets', $asset_id, 'asset_client_id'));
enforceClientAccess();
$asset_name = getFieldById('assets', $asset_id, 'asset_name');
$f = fopen('php://memory', 'w');
fputcsv($f, INTERFACE_CSV_COLUMNS, ',', '"', '\\');
fputcsv($f, ['Gi1/0/1', 'Uplink to firewall', 'Ethernet', '', '', '', '', '', 'Trunk 1, 10, 20'], ',', '"', '\\');
fputcsv($f, ['eth0', 'Management', 'Ethernet', '00:1A:2B:3C:4D:5E', 'DHCP', '', '', 'Office LAN', ''], ',', '"', '\\');
fseek($f, 0);
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="' . toAlphanumeric($asset_name) . '-Asset-Interfaces-Template.csv";');
fpassthru($f);
exit;
}
if (isExportRequest('export_asset_interfaces')) {
validateCSRFToken();
// Exports are reads - see CONTRIBUTING.md
enforceUserPermission('module_support');
$format = resolveExportFormat($_POST['export_asset_interfaces']);
$asset_id = intval($_POST['asset_id']);
$client_id = intval(getFieldById('assets', $asset_id, 'asset_client_id'));
enforceClientAccess();
$asset_name = getFieldById('assets', $asset_id, 'asset_name');
// Scoped to the one asset, so no page filters apply
$rows = [];
$sql = mysqli_query($mysqli, "SELECT asset_interfaces.*, network_name, network FROM asset_interfaces
LEFT JOIN networks ON network_id = interface_network_id AND interface_network_id > 0
WHERE interface_asset_id = $asset_id AND interface_archived_at IS NULL");
while ($row = mysqli_fetch_assoc($sql)) {
$rows[intval($row['interface_id'])] = $row;
}
$links = interfaceLinks(array_keys($rows));
interfaceSort($rows);
$num_rows = count($rows);
if ($num_rows > 0) {
guardExportPdfRowCount($format, $num_rows);
$export = beginExport('asset_interfaces', $format, toAlphanumeric($asset_name) . '-Interfaces', "$asset_name - Interfaces", '');
foreach ($rows as $row) {
$link = $links[intval($row['interface_id'])] ?? null;
$row['connected_to'] = $link ? $link['asset_name'] . ' ' . $link['interface_name'] : '';
addExportRow($export, $row);
}
finishExport($export);
}
logAudit("Asset Interface", "Export", "$session_name exported $num_rows interface(s) of asset " . escapeSql($asset_name) . " to a " . strtoupper($format) . " file", $client_id, $asset_id);
exit;
}