From a5a43e37ac01533ce874e9d8f5d7d371ca49d95e Mon Sep 17 00:00:00 2001 From: johnnyq Date: Mon, 19 Oct 2026 00:56:52 -0400 Subject: [PATCH] Audit logs stage 3: failed sign-ins say why and are logged against the account they were for - wrong password, no account with that email, disabled, archived or invited account, client archived, portal turned off, a wrong MFA or two-step code after the right password - with failed Microsoft 365 portal sign-ins logged too and marked Their account in the audit log; the IP lockout counts portal failures as well; the first sign-in from an IP address is badged New IP in the audit lists and quick look; new Admin > Sign-ins under Access with People and IP addresses views, search, date filter, sortable columns and paging; admins get a notification and an email on a run of failed sign-ins or turned-away API calls from one address or against one account, set in Settings > Security (10 to start, 5, 20, 50 or never), each in the audit log Security view and in Mail > Previews; API calls counted per key, day and endpoint, with a 30-day calls column and last IP on API Keys and a usage window, expired-key calls logged against the key, and usage deleted with the key and after the log retention; database 3.3.2 drops the unused auth_logs table and adds settings.config_login_alert_failures and api_key_usage; new functions/sign_in.php, admin/sign_ins.php and admin/modals/api/api_key_usage.php; the license template page no longer warns when view is missing --- CHANGELOG.md | 23 +- admin/api_keys.php | 26 ++- admin/audit_logs.php | 3 +- admin/database_updates/3.3.2.php | 39 ++++ admin/includes/side_nav.php | 6 + admin/modals/api/api_key_usage.php | 117 ++++++++++ admin/modals/audit_log/audit_log.php | 4 +- admin/post/api_keys.php | 2 + admin/post/settings_security.php | 8 +- admin/settings_security.php | 17 +- admin/sign_ins.php | 324 +++++++++++++++++++++++++++ admin/software_template.php | 2 +- agent/audit_log.php | 3 +- agent/user/user_activity.php | 3 +- api/v1/validate_api_key.php | 15 +- client/login_microsoft.php | 4 + cron/nightly_tasks.php | 4 +- db.sql | 38 ++-- functions.php | 1 + functions/api_keys.php | 135 +++++++++++ functions/audit_log.php | 23 +- functions/email_preview.php | 6 + functions/sign_in.php | 233 +++++++++++++++++++ functions/user.php | 18 +- login.php | 40 ++-- 25 files changed, 1022 insertions(+), 72 deletions(-) create mode 100644 admin/database_updates/3.3.2.php create mode 100644 admin/modals/api/api_key_usage.php create mode 100644 admin/sign_ins.php create mode 100644 functions/sign_in.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b3aea8f..f8d3a6215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,9 @@ This file documents all notable changes made to ITFlow. ### Upgrading to 26.11 -Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.3.1) in the same pass. Nothing else is required. +Update from Maintenance > Update — Queue Update hands the job to cron, which updates the files and applies this release's database changes (2.7.9 to 3.3.2) in the same pass. Nothing else is required. -This is a big release, with fifty-two database steps in one go, so take a backup before you update. +This is a big release, with fifty-three database steps in one go, so take a backup before you update. The update also clears out data that deletes in older versions left behind, all of it pointing at something that no longer exists: @@ -211,6 +211,12 @@ If you are coming from 26.08 or earlier, the database update still has to be run - New endpoint to read services, under the Support permission. Vendors take the account manager's phone and email. - A call the key's role doesn't allow is refused with what it needs and what the key's user has - "This needs Modify on Support - the key's user, Tess Tech (Technician), has Read." - in place of "The user linked to this API key does not have permission for this action." It is still a 403. Anything that matches on the old text needs changing. - Audit log for agents: each client now has an Audit Log, and record pages show their history, to anyone who can open them - not only admins. An agent sees the kinds of entry their role covers - a technician without Financial sees no expenses or trips, someone without Credentials no passwords looked at - and never sign-ins, users, settings or another agent turned away from a client, which stay in Admin > Audit Logs. The client overview's Recent activity follows the same rule, so it can show a restricted role less than before. +- Sign-ins: + - A failed sign in is now logged against the account it was to - a wrong password, or an account that is disabled, archived or not set up yet - where it used to name no one. So it shows in that person's Your account > Activity and in the sign-ins Admin > Users shows for them, even when someone else was guessing at it. + - A client portal contact's wrong two-step code now counts toward turning an IP address away (15 failed sign-ins in 10 minutes), as an agent's always did. + - Admins are now told about a run of failed sign-ins - 10 in 10 minutes from one IP address or against one account - with a notification and an email each. Change or turn it off in Admin > Settings > Security. + - A call to the API with a key that has expired now says which key, and is logged against it. Anything that reads the `logs` table for "Incorrect or expired key" needs the new wording. + - The `auth_logs` table is removed. Nothing was ever written to it. - Audit log: each thing done now has one name, in the entries already there as well as new ones. Unarchive and Restored are Restore, Deleted is Delete, and Closed, Resolved, Reopened, Merged and Sent are Close, Resolve, Reopen, Merge and Send. An agent turned away from a client they can't open is Denied, where it was Access. A saved card or payment provider deleted is Delete and a card saved is Create, where both were Update, and a contract template changed is Edit. A report or script that reads the `logs` table by the old names needs the new ones. ### New Features & Updates @@ -943,6 +949,11 @@ If you are coming from 26.08 or earlier, the database update still has to be run - The client overview's Recent activity links to the client's Audit Log, for everyone rather than admins only. - Your account > Activity is rebuilt on the same list: everything you did, searched and filtered by date, a page at a time, with views for Changes, Deletions, Sign-ins and Data out, each entry with its client, its record and where you did it from. Sign-ins says what to do about one that wasn't you. - A document's earlier versions are now in a card called Versions, with its history under it. + - A failed sign in says why - a wrong password, no account with that email, an account that is disabled, archived or hasn't set a password yet, a contact whose client is archived, the client portal turned off, or a wrong MFA or two-step code after the right password - and is on the account it was to, marked Their account, as it may not have been them. A sign in through Microsoft 365 that fails is logged too. + - The first time an account signs in from an IP address, the sign in is marked New IP - in Admin > Audit Logs, the client's Audit Log, Your account > Activity and the quick look. + - Admin > Sign-ins, under Access: People - each agent and portal contact with their last sign in, its IP address and device, how many sign-ins and failed ones (the wrong MFA codes among them), how many IP addresses and, for a date range, how many of them were new - and IP addresses - each address with its sign-ins and failures, the accounts it tried and any emails with no account, when it was first and last seen, and whether it is being turned away now. Searched by name, email or address, for a date range, sorted from the column headings, a page at a time; each opens its sign-ins in the audit log. + - Failed sign-in alerts: after a number of failed sign-ins in 10 minutes from one IP address or against one account - 10 to start with, or 5, 20, 50 or never, in Admin > Settings > Security - each admin gets a notification and an email saying who was tried and from where, with a link to look. A run is told about once. The same goes for API calls turned away from one address. Each alert is in the audit log's Security view. + - Admin > API Keys counts each key's calls over the last 30 days and shows where the last one came from. Usage, on a key's menu or its count, shows its calls today, this week and this month, each day's as a bar, the endpoints it called and how often, the IP addresses the calls came from, and the calls turned away after it expired. Use is kept as long as the audit log. - **Emails** (rebuilt) - Ticket emails to clients now look like the billing emails: your logo or name, a coloured heading saying what it is, the ticket's number, subject, status and priority in a box, the reply or the details quoted as they were written with who wrote them, a button to open the ticket, and your details at the foot. This covers a ticket opened by an agent, on a schedule or from an email, a reply, resolved, closed, being made a watcher, replying to a closed ticket, a reply through the API and a task needing their approval. - Each says under it why they get it - the contact on the ticket, watching it, or asking for every update on their company's tickets - in place of the YOU ARE A COLLABORATOR block. @@ -964,7 +975,7 @@ If you are coming from 26.08 or earlier, the database update still has to be run - Shared links: the email with a link and the one with a code to open it are in the same look, with your note quoted and the whole link printed under the button, as a link only works when it is copied whole. Subjects are the same. - Calendar invitations, updates and cancellations say when, how often it repeats, where, the description and a Join button for a meeting link, with the calendar file as before. - Email on the contacts and clients lists: what you write goes out with your logo above it, your details below and a line saying why they got it. Pick the mailbox it comes from and the name it shows, start it "Hello" and their name if you like, and send it now or later. What you write is made safe and its images point at your ITFlow, as in ticket emails. -- **Admin > Mail > Previews**: every email ITFlow sends, 52 of them, as it goes out - your logo, your details and the address it comes from. Each is made when you open it, by the same code that sends it, on sample records (a client, its contacts and an agent) that are thrown away again, so looking sends and saves nothing. It says who gets it and what sends it, shows it at computer or phone width, and Send me a copy emails it to you, marked [Preview]. +- **Admin > Mail > Previews**: every email ITFlow sends, 53 of them, as it goes out - your logo, your details and the address it comes from. Each is made when you open it, by the same code that sends it, on sample records (a client, its contacts and an agent) that are thrown away again, so looking sends and saves nothing. It says who gets it and what sends it, shows it at computer or phone width, and Send me a copy emails it to you, marked [Preview]. ### Bug Fixes @@ -1598,6 +1609,12 @@ If you are coming from 26.08 or earlier, the database update still has to be run - New `agent/audit_log.php` (`client_id` - without it, admins go to Admin > Audit Logs and agents to the clients - `q`, `user`, `type`, `action`, `entity`, `view` - `changes`, `deletions`, `signins` or `data` - the date filter, `sort` - `log_created_at`, `user_name`, `log_type`, `log_action`, `log_ip` or `log_id` - `order` and `page`), scoped to the client and `auditLogRoleSql()`, in `client_side_nav.php` after Calendar. `clientOverviewActivity()` applies `auditLogRoleSql()`. - `agent/user/user_activity.php` is rebuilt (`q`, `view`, the date filter, `sort` - `log_created_at`, `log_type`, `log_action`, `client_name`, `log_ip` or `log_id` - `order` and `page`), on the agent's own entries. - The admin quick look's links to itself are absolute, so it opens from agent pages too. + - Database 3.3.2: `auth_logs` is dropped (never written; nightly only emptied it); `settings.config_login_alert_failures` (int, 10; 0 never) - read with `signInAlertLimit()` rather than with every page's settings, so pages open before the update adds it; new `api_key_usage` (`api_key_usage_key_id`, `api_key_usage_date`, `api_key_usage_endpoint`, `api_key_usage_calls`, `api_key_usage_last_at`, `api_key_usage_last_ip`; primary key the first three). Nightly deletes `api_key_usage` older than `config_log_retention` in place of `auth_logs`. + - New `functions/sign_in.php`: `SIGN_IN_WINDOW_MINUTES` (10), `SIGN_IN_LOCKOUT` (15), `SIGN_IN_TYPES` (Login, Client Login), `SIGN_IN_FAILED` (Failed, MFA Failed); `signInAlertOptions()`, `signInAlertLimit()`, `signInFailureReason($email)` (`user_id`, `client_id`, `name`, `type`, `reason`; an agent's account before a portal one), `signInLog()` (as the account, without signing in), `signInLogFailure()`, `signInWatch($user_id)` (on reaching the limit from the IP address or against the account), `signInAccountsTried()`, `signInAdmins()`, `signInAlertAdmins($title, $text, $summary, $url)` (a Failed Sign-ins notification and an email to each active admin, and Login Alert in the log), `signInMailAlert()` (in Mail > Previews as `admin_sign_in_alert`), `signInNewIpIds($rows)` and `signInNewIpBadge()`. + - `login.php`: the IP lockout counts both sign-in types' failures, with the constants; a password that matches no account is logged with `signInFailureReason()`, as Login or Client Login on that account; an agent's wrong MFA code, a contact's wrong two-step code, the portal turned off and a contact's account that can't sign in with a password go through `signInLogFailure()`. `client/login_microsoft.php` logs a failed Entra sign in. Blocked reads "Sign in turned away from IP - 15 failed sign-ins in 10 minutes". `userLoginLineHtml()` shows why a sign in failed, and `userRecentLogins()` includes the failures against the account. + - `auditLogWho()` marks a failed sign in Their account; Security also takes Login Alert and API Failed; Alert is a danger action. Admin > Audit Logs, the client's Audit Log, Your account > Activity and the quick look badge `signInNewIpIds()`. + - New `admin/sign_ins.php` (`view` - '' People or `ips` - `q`, the date filter, `sort` - People `user_name`, `sign_ins`, `failures`, `ips` or `last_seen`, IP addresses `log_ip`, `sign_ins`, `failures`, `accounts`, `first_seen` or `last_seen` - `order`, `page`), in the admin nav under Access. Settings > Security takes `config_login_alert_failures` (one of `signInAlertOptions()`, else 10). + - API keys, in `functions/api_keys.php`: `apiKeyUsageEndpoint($uri)`, `apiKeyUsageRecord($id, $endpoint, $ip)` (an upsert per call; nothing before the update), `apiKeyUsageSummary($ids, $days)` (`calls`, `endpoints`, `last_at`, `last_ip`), `apiKeyUsageDaily()`, `apiKeyUsageEndpoints()`, `apiKeyUsageIps()`, `apiKeyTurnedAway()` and `apiKeyWatch()`. `api/v1/validate_api_key.php` records each call, and logs a call turned away as "Expired API key NAME used - it ended DATE" with `log_entity_id` the key, or "Unknown API key used". New `admin/modals/api/api_key_usage.php` (`id`); deleting a key deletes its use. - Empty states: every list page sets `$empty_state` before including `includes/filter_footer.php`, and `includes/inc_empty_state.php` draws it with `emptyStateHtml()` (`functions/format.php`) when the list has nothing - `thing`, `icon`, `title`, `text`, `actions` (`label`, `modal` or `href`, `size`, `icon`; the first is the main one), `archived`, `view`, `filtered`, `views`, `match_title`, `match_text` and `clear_url`. `emptyStateFiltered($views)` reads the GET string for a search or filter, leaving out paging, sorting, `client_id`, `archived` and the page's views, and `emptyStateClearUrl($views)` is the page without them. `emptyStatePill($pills, $key, $default, $good)` builds a pill's own state from the page's pill array (`label`, `icon`, `title`, `alarm`), with a tick for `alarm` pills and the keys in `$good`. Files, notifications and racks call `emptyStateHtml()` in their own layout; files and notifications set `$empty_state_skip`. A page that sets nothing still gets one, worded from `$page_title`. The pages' own empty blocks (`.itflow-asset-empty` under a list) are gone. CSS: `.itflow-empty-state` with `-icon` (a grey round tile), `-title`, `-text` and `-actions`. - Removed unused helpers and files: - `credentialLink()`, `projectLink()`, `linkCounts()`, `assetContactAvatar()`, `clientOverviewPersonInitials()`, and `getMonthlyTax()` and `getQuarterlyTax()`, which looked a tax up by name diff --git a/admin/api_keys.php b/admin/api_keys.php index 7ca0f48bb..88431ec33 100644 --- a/admin/api_keys.php +++ b/admin/api_keys.php @@ -4,9 +4,10 @@ * Admin > API Keys - the keys other systems use the API with. Each acts as * an agent, so each shows who, what their role opens and which clients * they see, whether the key works (it expires, and an agent who is - * disabled, archived or has no role turns every call away) and when it was - * last used. A revoked key has expired, so it moves to Expired, where it can - * be deleted. The work is in functions/api_keys.php; the handlers are + * disabled, archived or has no role turns every call away), when it was + * last used and from where, and its calls over the last 30 days - each + * opening what it was used for (modals/api/api_key_usage.php). A revoked key + * has expired, so it moves to Expired, where it can be deleted. The work is in functions/api_keys.php; the handlers are * admin/post/api_keys.php. */ @@ -49,6 +50,8 @@ while ($row = mysqli_fetch_assoc($sql)) { } $roles = roleRows(); $access = userClientAccess(array_filter(array_map('intval', array_column($keys, 'api_key_user_id')))); +// What each is used for, the last 30 days (functions/api_keys.php) +$usage = apiKeyUsageSummary(array_keys($keys)); $counts = mysqli_fetch_row(mysqli_query($mysqli, "SELECT COALESCE(SUM(api_key_expire > '$today'), 0), COALESCE(SUM(api_key_expire <= '$today'), 0) FROM api_keys LEFT JOIN users ON user_id = api_key_user_id WHERE $search")); @@ -117,6 +120,7 @@ $csrf = $_SESSION['csrf_token']; Can open + Calls (30 days) @@ -127,6 +131,11 @@ $csrf = $_SESSION['csrf_token']; $expired = apiKeyExpired($row); $agent_name = trim(str_replace(' (archived)', '', (string) $row['user_name'])); $last_used = $row['api_key_last_used_at'] ? '' . escapeHtml(timeAgo($row['api_key_last_used_at'])) . '' : 'Never'; + $use = $usage[$api_key_id] ?? null; + if ($use && $use['last_ip'] !== '') { + $last_used .= '
' . escapeHtml($use['last_ip']) . '
'; + } + $usage_url = "modals/api/api_key_usage.php?id=$api_key_id"; ob_start(); ?>
@@ -165,6 +174,14 @@ $csrf = $_SESSION['csrf_token']; + + + +
endpoint
+ + None + +