Repository navigation
Demo Data Deleted Real Accounts
Importing the Core demo data on a working installation deleted every analyst except one, every self-service requester, and the departments, teams and permission roles alongside them. The person it happened to then found he could not sign back in, permanently, and that re-creating his account by hand did not help.
Reported by Kristian Madsen - the same person as issue #117, immediately after that one was settled.
Fixed in f3fe850e, a99f630b, f935d8c8 and ace526af, released as updates #1296, #1297 and #1298.
This is really three faults wearing one coat, and the second one is the interesting one, because it only became visible because the first one was fixed badly a long time ago.
"I had an account that was made with OIDC and the account was working fine. After I imported the demo data and users, it removed my account both on service and analyst. It gives me this error when trying to sign in - 'Your account is no longer available. Contact your service desk.' - it doesn't exist anywhere in the admin interface."
Two symptoms, and it matters that they are separate:
- The account is gone from the admin interface. That is the importer.
- Signing in is impossible, and stays impossible even after an administrator re-creates the account with the same email address. That is the sign-in code, and it would have happened to anyone whose account was deleted by any means that skipped the database's own housekeeping.
Before inserting anything, the importer cleared each table it was about to fill. It had to - otherwise importing twice would give you two of everything. But nothing distinguished a sample row from a real one, so "clear the table" meant the whole table:
DELETE FROM analysts WHERE NOT (username = 'admin') -- one hardcoded survivor
DELETE FROM users -- no exemption at allThe single exemption for admin comes from a _skip_insert marker in database/demo-data/core.json, which exists so the importer can reference the admin account rather than create one. It was never a safety mechanism, and it protected exactly one row.
Importing Core therefore removed:
| Table | What that is |
|---|---|
analysts |
every analyst except admin
|
users |
every self-service requester, with no exemption |
departments, teams
|
the whole organisational structure |
rbac_roles, rbac_role_capabilities
|
the permission model - 80 capability rows |
ticket_types, ticket_origins, ticket_prefixes
|
lookup values that existing tickets point at |
"Designed for fresh installations only. Importing demo data into a system that already contains real data may cause conflicts."
It does not cause conflicts. It deletes. And the confirmation dialog - which does exist - was only shown when the button had already been used in that page session, so the very first import, the one that takes an installation from having no demo data to having it, was the only one that never asked.
This is the half that turned a bad afternoon into a permanent lock-out.
Both identity tables declare the correct constraint:
CONSTRAINT `fk_user_sso_identity_user` FOREIGN KEY (`user_id`)
REFERENCES `users` (`id`) ON DELETE CASCADEDelete a requester and their sign-in link goes with them. That is why nobody had ever hit this using the admin interface.
But the importer ran its deletes like this:
$conn->exec("SET FOREIGN_KEY_CHECKS = 0");Turning foreign key checks off does not only skip the checks. It skips the cascades. The accounts were deleted; the links survived, pointing at ids that no longer existed.
Every sign-in path resolves a person in the same three steps:
- an existing link, by (provider, subject),
- failing that, match an existing account by verified email,
- failing that, create one just in time.
Step 1 runs first, and the branch was chosen on "is there a link row", not "is there an account":
$userId = $stmt->fetchColumn(); // finds the orphan
if ($userId) { // true - so we are committed to "returning user"
$user = ssLoadUser($conn, $userId);
if (!$user) {
ssoBail('Your account is no longer available. Contact your service desk.');
}Steps 2 and 3 live in the else. He could never reach them. And that is why re-creating his account did not help - the link still pointed at the old id, so the new account was never even looked for.
Had the cascade been allowed to fire, nobody would ever have noticed any of this. The link would have gone with the account, the next sign-in would have fallen through to just-in-time provisioning, and the account would have quietly reappeared. The bug that made the damage permanent is the same line that made it silent.
The convention here is to sweep for other instances before writing anything up, and this one paid for itself. The same resolve-by-link-first shape exists in the LDAP and Active Directory path:
| File | Function | What it said when the account was missing |
|---|---|---|
api/auth/oidc_callback.php |
analyst branch | "Your account is inactive" - for an account that does not exist at all |
api/auth/oidc_callback.php |
completeSelfServiceSso() |
"Your account is no longer available" |
includes/ldap.php |
ldapResolveAnalyst() |
"Your account is inactive" |
includes/ldap.php |
ldapResolveUser() |
"Your account could not be found" |
Four branches, four different messages, one fault. Two of the four messages actively name the wrong cause: an account that has been deleted is not an account that is inactive, and an administrator sent looking for a disabled account will not find one.
The handling now lives in one place, includes/sso_identity.php, required by both includes/oidc.php and includes/ldap.php.
ldapResolveAnalyst() re-links the identity at the end, inside a try/catch that deliberately swallows a unique-key failure - a reasonable guard against a concurrent sign-in. But it means that if you fix only the fall-through and not the stale row, the person signs in and the link goes on pointing at the deleted id for ever, with nothing anywhere reporting it. The first version of the test for this passed with the fix disconnected, for exactly that reason. It only became a real test once it asserted where the link ended up, not just that sign-in succeeded.
Every row the importer creates now carries is_demo = 1, on all 73 tables it writes to, and only marked rows are ever removed:
DELETE FROM `$tableName` WHERE is_demo = 1The mark is set in the importer rather than in the JSON, so it cannot be forgotten when a new demo file is written.
SET FOREIGN_KEY_CHECKS = 0 is gone. With checks on, a demo row that something else still refers to cannot be silently orphaned - the delete fails, the transaction rolls back, and the import reports what is in the way:
"The Tickets demo data still refers to the Core demo data, so it cannot be replaced while that is in place. Remove or re-import Tickets first. Nothing has been changed."
This is a deliberate loss of capability. Re-importing Core while the Tickets demo data exists used to "work". It worked by stranding rows.
A link whose account is gone is now cleared, and the sign-in continues down the ordinary first-time path - the same verified-email and provider-assignment checks a new user faces. Nothing becomes reachable that a first-time user could not already reach: deleting an account through the interface already cascades the link away and already permits just-in-time re-creation.
- The confirmation is asked before every import, not only a repeat.
- The warning says what actually happens, and that your own records are not touched.
- Installations holding demo data from before the mark existed are told so. There is no way to tag it retrospectively - nothing ever recorded which rows came from where - so the honest move is to say it rather than quietly produce a second copy.
- The duplicate-key failure that such an installation hits is now explained, rather than reported as
Duplicate entry 'jsmith' for key 'analysts.uq_analysts_username'- a message naming a record the administrator never created.
The page worked out whether a module had been imported by asking "does this table have any rows in it?" for most modules. So an installation with real tickets, or real assets, was told its demo data was already imported. Same root gap - nothing distinguished demo from real - showing a second face on the same screen. It now counts marked rows, and covers all 19 modules instead of five.
ποΈ schema Β· π read Β· βοΈ write Β· π₯οΈ UI Β· π strings Β· π§ͺ test
| π¨ | File | What you do there |
|---|---|---|
| ποΈ | database/freeitsm.sql |
is_demo on 73 tables |
| ποΈ | includes/db_verify_schema.php |
the same 73, so existing installations gain the column |
| βοΈ | api/system/import_demo_data.php |
marked-rows-only clearing, checks left on, explained failures |
| π | includes/demo_data.php |
new. The one home for module β tables, demo row counts, blocking-module lookup, untagged detection |
| π | api/system/check_demo_core.php |
counts marked rows instead of asking whether a table has any |
| βοΈ | includes/sso_identity.php |
new. ssoClearDanglingLink(), shared by both sign-in paths |
| βοΈ | api/auth/oidc_callback.php |
both branches fall through instead of dead-ending |
| βοΈ | includes/ldap.php |
both resolvers, the same |
| π₯οΈ | system/demo-data/index.php |
confirm on every import, untagged banner, per-module detection |
| π | lang/*/system.php |
rewritten warning; new confirmations in the 7 locales carrying this namespace |
| π§ͺ | tests/sso-dangling-link.php |
12 assertions across both sign-in paths |
Two kinds of proof, because neither is sufficient alone.
The test suite covers the sign-in half: that the cascade works when checks are on, that the orphan appears when they are off, that clearing it restores the fall-through, that a healthy link on the same provider is untouched, and that the LDAP resolver ends up pointing at the re-created account. Proved load-bearing by disconnecting the fix - 12 passing becomes 11 passing and 1 failing.
A live run covers the importer, on a throwaway database built from database/freeitsm.sql: all 19 modules import; a real analyst and a real requester survive an import and a re-import with their ids unchanged; a re-import replaces rather than duplicates; and the cross-module refusal rolls back leaving nothing changed. It was the live run, not the tests, that found the re-import refusal - the tests were written against the design and would have gone on agreeing with it.
Then the same thing on a real development database with 108 tickets, 68 requesters and 7 analysts in it, checked with a row-count snapshot of all 264 tables before and after. The only difference across the whole database was the SLA cron ticking over during the test.
The first attempt at the throwaway run appeared to fail: Duplicate entry 'jsmith' on a database that had just been created empty. It had not failed. PHP's opcache was still serving the previous database configuration file, so the import had been running against the real development database all along.
Nothing was damaged, and why nothing was damaged is the point: the delete phase ran, matched no rows because nothing was marked, and the insert then collided and rolled back. Under the old code that same slip would have emptied analysts and users - which is to say the accident reproduced the original bug's conditions exactly and demonstrated the fix at the same time.
Every subsequent run went through a gate: a probe page that calls opcache_reset(), connects, and returns SELECT DATABASE(), with the harness refusing to continue unless the answer is the throwaway. If you swap a database configuration under a running web server, do not trust that it took effect. Ask the web server.
- If you have never imported demo data, nothing here affects you, and importing is now safe on a populated installation: only rows the importer created are ever removed.
- If you imported demo data before update #1297, those rows are not marked and cannot be recognised. The page will tell you. Re-importing would add a second copy, so remove the old sample records by hand first if you want a clean set.
-
If you have lost accounts to this, the recovery is two statements, and the local
adminanalyst is the way back in because it is the one row the importer spared:
DELETE FROM user_sso_identities WHERE user_id NOT IN (SELECT id FROM users);
DELETE FROM analyst_sso_identities WHERE analyst_id NOT IN (SELECT id FROM analysts);Then sign in again. The account is re-created if your provider allows it, or matched against one an administrator re-creates with the same email address. On any installation running #1296 or later this happens by itself and the statements are unnecessary.
- There is no "remove demo data" action. Only import. So when a module blocks a re-import, the way out is to re-import the blocking module rather than clear it, which is a blunt instrument.
-
Untagged demo data cannot be recognised, and no amount of cleverness will change that - the information was never recorded. The detection probe looks for an analyst named
jsmith, which is a guess, not a fact: a real person with that username would be reported as untagged demo data. - Whether a cross-module re-import should cascade - clearing Core's demo rows also clearing the demo rows that depend on them - is undecided. It would be safe, since only marked rows are ever touched, but it deletes things the administrator did not name.
- Sign-in redirected to the wrong address - the same reporter, the week before
- Single sign-on Β· LDAP and Active Directory
-
Database verification - how the
is_democolumn reaches an existing installation
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96