diff --git a/admin/post/demo_data_model.php b/admin/post/demo_data_model.php index dd116be30..c1b9ee85f 100644 --- a/admin/post/demo_data_model.php +++ b/admin/post/demo_data_model.php @@ -367,6 +367,17 @@ function demoFirstId($mysqli, $table, $id_column, $archived_column = '') { // Deterministic stand-in for a serial number - same input, same serial, so a // reload after a removal does not produce a whole new set of numbers +/** A 2FA seed that comes out the same on every run - 32 base32 characters from the seed. */ +function demoTotpSeed($seed) { + $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + $bytes = md5('itflow-demo-totp-' . $seed, true) . md5('itflow-demo-totp-2-' . $seed, true); + $out = ''; + for ($i = 0; $i < 32; $i++) { + $out .= $alphabet[ord($bytes[$i]) % 32]; + } + return $out; +} + function demoSerial($seed, $length = 10) { return strtoupper(substr(md5('itflow-demo-' . $seed), 0, $length)); } @@ -1342,9 +1353,12 @@ function demoBuildClient($mysqli, $profile, $index, $context, &$counts) { 'credential_password' => encryptCredentialEntry(demoSerial($profile['abbreviation'] . $credential[0], 14) . '!aA1'), 'credential_note' => $credential[4], 'credential_favorite' => $credential[6], + // A 2FA seed on the logins that have one, so the codes count down like the real thing + 'credential_otp_secret' => !empty($credential[7]) ? demoTotpSeed($profile['abbreviation'] . $credential[0]) : '', 'credential_client_id' => $client_id, 'credential_created_at' => demoMonthDateTime($months, 8, 16, 10), - 'credential_password_changed_at' => demoMonthDateTime(max(0, min(4, $months)), 8, 16, 10), + // Some changed lately, some due, one well over a year - never before the client was taken on + 'credential_password_changed_at' => demoDateTime(min($credential[8] ?? 30, max(0, $months * 30 - 10)), 16, 10), 'credential_accessed_at' => demoDateTime(($index + 2) % 15, 11, 30), ]); $credential_ids[$credential[0]] = $credential_id; @@ -1480,6 +1494,8 @@ function demoBuildClient($mysqli, $profile, $index, $context, &$counts) { foreach ($assets as $asset) { if (!empty($asset['credential']) && $asset['credential'] === $credential_name) { demoLink($mysqli, 'asset_credentials', 'credential_id', $credential_id, 'asset_id', $asset['id']); + // The credential's own asset - what the asset's Credentials tab and the credential show + mysqli_query($mysqli, "UPDATE credentials SET credential_asset_id = " . intval($asset['id']) . " WHERE credential_id = " . intval($credential_id) . " AND credential_asset_id = 0"); } } } @@ -4500,13 +4516,14 @@ function demoExpandProfile($spec) { // Credentials - names line up with demoAssetCredentialName so the vault // entries attach to the kit they actually open $mail_admin_uri = $mail === 'Google Workspace' ? 'https://admin.google.com' : 'https://admin.microsoft.com'; + // [name, description, uri, username, note, tags, starred, has 2FA, password last changed (days ago)] $credentials = [ - ['Firewall Administrator', 'Edge firewall management interface', 'https://192.168.10.1', 'msp-admin', 'Local administrator on the edge firewall. Rotated at every quarterly review.', ['Firewall', 'Local Admin', 'Rotate Quarterly'], 1], - [$mail . ' Administrator', 'Tenant administration for mail and identity', $mail_admin_uri, 'msp-admin@' . $domain, 'Break glass administrator. MFA enforced - the token lives with the on call phone.', ['Microsoft 365', 'Domain Admin', 'Break Glass'], 1], - ['Hypervisor and Storage Administrator', $foss ? 'Proxmox VE and TrueNAS management' : 'Hypervisor and NAS management', 'https://192.168.20.10:8006', 'msp-admin', 'Root equivalent. Use the individual accounts for day to day work and this only for maintenance.', ['Hypervisor', 'Local Admin', 'Break Glass'], 0], - ['Network Controller', 'UniFi controller for switching and wireless', 'https://192.168.10.12', 'msp-' . strtolower($short), 'Controller runs on the management stack, not on site.', ['Switch', 'Wireless', 'Shared'], 0], - ['Backup Console', $foss ? 'Proxmox Backup Server and Synology Active Backup' : 'Backup platform for servers and endpoints', 'https://backup.example', 'msp-' . strtolower($short), 'Console login for restore requests and job monitoring.', ['Backup Console', 'Service Account'], 0], - ['Registrar and DNS', 'Domain registrar and DNS control panel', 'https://registrar.example', 'billing@' . $domain, 'Registrar account is in the client name with us as technical contact.', ['Registrar and DNS', 'Shared'], 0], + ['Firewall Administrator', 'Edge firewall management interface', 'https://192.168.10.1', 'msp-admin', 'Local administrator on the edge firewall. Rotated at every quarterly review.', ['Firewall', 'Local Admin', 'Rotate Quarterly'], 1, 0, 25], + [$mail . ' Administrator', 'Tenant administration for mail and identity', $mail_admin_uri, 'msp-admin@' . $domain, 'Break glass administrator. MFA enforced - the token lives with the on call phone.', ['Microsoft 365', 'Domain Admin', 'Break Glass'], 1, 1, 120], + ['Hypervisor and Storage Administrator', $foss ? 'Proxmox VE and TrueNAS management' : 'Hypervisor and NAS management', 'https://192.168.20.10:8006', 'msp-admin', 'Root equivalent. Use the individual accounts for day to day work and this only for maintenance.', ['Hypervisor', 'Local Admin', 'Break Glass'], 0, 0, 430], + ['Network Controller', 'UniFi controller for switching and wireless', 'https://192.168.10.12', 'msp-' . strtolower($short), 'Controller runs on the management stack, not on site.', ['Switch', 'Wireless', 'Shared'], 0, 0, 60], + ['Backup Console', $foss ? 'Proxmox Backup Server and Synology Active Backup' : 'Backup platform for servers and endpoints', 'https://backup.example', 'msp-' . strtolower($short), 'Console login for restore requests and job monitoring.', ['Backup Console', 'Service Account'], 0, 1, 210], + ['Registrar and DNS', 'Domain registrar and DNS control panel', 'https://registrar.example', 'billing@' . $domain, 'Registrar account is in the client name with us as technical contact.', ['Registrar and DNS', 'Shared'], 0, 1, 15], ]; // Services - the last two entries say which asset types run it and which diff --git a/agent/ajax.php b/agent/ajax.php index aa53fc16d..fa9921bb2 100644 --- a/agent/ajax.php +++ b/agent/ajax.php @@ -817,6 +817,10 @@ if (isset($_GET['share_generate_link'])) { // Logging logAudit("Share", "Create", "$session_name created shared link for $item_type - $item_name", $client_id, $item_id); + // ...and on the credential itself, so its history says who it went out to + if ($item_type == "Credential") { + logAudit("Credential", "Share", "$session_name shared credential $item_name" . ($item_email ? " with $item_email" : '') . ", expiring in $item_expires_friendly", $client_id, $item_id); + } } @@ -1167,13 +1171,19 @@ if (isset($_GET['get_totp_token_via_id'])) { $credential_id = intval($_GET['credential_id']); $sql = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT credential_name, credential_otp_secret, credential_client_id FROM credentials WHERE credential_id = $credential_id")); + if (!$sql) { + echo json_encode(''); + exit; + } $name = escapeSql($sql['credential_name']); $totp_secret = $sql['credential_otp_secret']; $client_id = intval($sql['credential_client_id']); enforceClientAccess(); - $otp = TokenAuth6238::getTokenCode(strtoupper($totp_secret)); + // Seeds saved before they were tidied on save may still carry spaces, dashes or the whole otpauth:// link + $totp_seed = credentialOtpSeed($totp_secret); + $otp = $totp_seed ? TokenAuth6238::getTokenCode($totp_seed) : ''; echo json_encode($otp); // Logging @@ -1816,6 +1826,10 @@ if (isset($_GET['get_credential_via_id'])) { $credential_id = intval($_GET['credential_id']); $sql = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT credential_name, credential_username, credential_password, credential_client_id FROM credentials WHERE credential_id = $credential_id")); + if (!$sql) { + echo json_encode(['error' => 'That credential no longer exists.']); + exit; + } $name = escapeSql($sql['credential_name']); $client_id = intval($sql['credential_client_id']); @@ -1827,6 +1841,9 @@ if (isset($_GET['get_credential_via_id'])) { ); echo json_encode($response); + // When it was last used - the credentials list sorts by it. Kept off the updated date, which means an edit + mysqli_query($mysqli, "UPDATE credentials SET credential_accessed_at = NOW(), credential_updated_at = credential_updated_at WHERE credential_id = $credential_id"); + // Only log if this user hasn't viewed this credential recently (mirrors TOTP dedup) $check_recent_view = mysqli_fetch_assoc(mysqli_query($mysqli, "SELECT COUNT(log_id) AS recent_view FROM logs WHERE log_type = 'Credential' AND log_action = 'View' AND log_user_id = $session_user_id AND log_entity_id = $credential_id AND log_client_id = $client_id AND log_created_at > (NOW() - INTERVAL 5 MINUTE)")); diff --git a/agent/asset.php b/agent/asset.php index d2b369f2a..72b8e4383 100644 --- a/agent/asset.php +++ b/agent/asset.php @@ -103,6 +103,7 @@ if (mysqli_num_rows($sql) == 0) { $can_edit = lookupUserPermission('module_support') >= 2; $can_delete = lookupUserPermission('module_support') >= 3; $can_credentials = lookupUserPermission('module_credential') >= 1; + $can_credentials_edit = lookupUserPermission('module_credential') >= 2; // The rack it sits in, if any $asset_rack = rackPositionForAsset($asset_id); @@ -211,18 +212,8 @@ if (mysqli_num_rows($sql) == 0) { $document_count = mysqli_num_rows($sql_related_documents); // Credentials - $credential_count = 0; - if ($can_credentials) { - $sql_related_credentials = mysqli_query($mysqli, " - SELECT credential_id, credential_name, credential_description, credential_uri, - credential_username, credential_otp_secret - FROM credentials - WHERE credential_asset_id = $asset_id - AND credential_archived_at IS NULL - ORDER BY credential_name ASC - "); - $credential_count = mysqli_num_rows($sql_related_credentials); - } + $related_credentials = $can_credentials ? credentialRows("credential_asset_id = $asset_id AND credential_client_id = $client_id") : []; + $credential_count = count($related_credentials); // Licenses it holds a seat on, with each one's seats in use and the key it was given $held_software = softwareHeldBy('asset', $asset_id); @@ -337,8 +328,8 @@ if (mysqli_num_rows($sql) == 0) { Recurring Ticket - - + + Credential @@ -365,7 +356,7 @@ if (mysqli_num_rows($sql) == 0) { License - + Credential @@ -734,98 +725,24 @@ if (mysqli_num_rows($sql) == 0) {
| Name | -Username | -Password | -OTP | -URI | -- |
|---|---|---|---|---|---|
|
- = $credential_name ?>
- = $credential_description ?>
- |
-
-
- = $credential_username ?> |
-
- |
- - - Hover... - - - - - | -
-
- = truncate($credential_uri, 26) ?> |
-
-
-
- |
-
| Name | Username | Password | OTP | URI | |
|---|---|---|---|---|---|
|
- = escapeHtml($row['credential_name']) ?>
- = $credential_description ?>
- |
- = $credential_username ?> |
-
- |
- - - Hover... - - - | -
- = truncate($credential_uri, 26) ?> |
-
-
-
- |
-