-
Notifications
You must be signed in to change notification settings - Fork 15
Extending Directory Sync
Recipes for changing the feature, and the checklists that stop a change being half-applied. How it works is Directory sync β Developer Guide.
Every list here was produced by tracing an existing field or flavour through the code, not from memory. If you add something and the list looks wrong, trace it again and fix the page β a stale checklist here is worse than none, because it will be trusted.
The canonical example is office, which appears in fourteen places. Miss one and the failure is usually silent: the column exists, the mapping box exists, and nothing ever writes to it.
| File | What to add |
|---|---|
database/freeitsm.sql |
users.<field>, and auth_providers.ldap_attr_<field>
|
includes/db_verify_schema.php |
The same two columns, in the users and auth_providers blocks |
β οΈ Both files, always.freeitsm.sqlis the fresh install; the$schemaarray is the upgrade. Dev only ever exercises the upgrade path, so a missingfreeitsm.sqlentry is invisible until somebody installs from scratch.
| Function | What to add |
|---|---|
dsyncAttrDefaults() |
The default attribute name for both flavours β the ad and the openldap arrays |
dsyncFetchPeople() |
dsyncAttr($provider, '<field>') in the $attrs list, or the attribute is never requested |
dsyncMapPerson() |
Read it out of the entry |
dsyncApplyToExisting() |
Add to the $map array, so a change is detected and logged |
dsyncCreate() |
The column and value in the INSERT
|
Optionally dsyncFieldLabel() (so the run log says "Office" and not office) and dsyncNewPersonSummary() (so it shows on a "will be added" row).
| Constant | Meaning |
|---|---|
USER_PERSON_FIELDS |
It is a person field, so api/tickets/save_user.php will accept it |
USER_DIRECTORY_OWNED |
The directory owns it, so it is refused on a managed record rather than accepted and then silently overwritten by the next sync |
userPersonFieldValue() if the value needs normalising.
| File | What to add |
|---|---|
api/system/save_sso_provider.php |
The $ldap array entry, the $cols list and the $vals list β these are positional, so they must stay in step |
system/sso/provider.php |
A row in $mapRows, and the field in payload()
|
api/system/test_directory_mapping.php |
The form-value passthrough list and the $fields array, or Test will not show it |
| File | What to add |
|---|---|
lang/en/system.php |
map_field_<field> and map_hint_<field>. An empty hint is fine; a missing key renders the key itself |
tests/people-fields.sh |
An assertion, so the next person to add a field finds out when they miss one |
π A quick way to check you got them all:
grep -rn "office" includes/ api/ system/ lang/en/ database/ tests/and compare the file list against yours.
dsyncFlavour() decides between ad and openldap, and it does so by inference, not configuration:
// Whatever ldap_attr_username / ldap_attr_guid were set to already tells us
// which directory this is. Asking again would let the two answers disagree.dsyncAttrDefaults($flavour) then supplies the attribute names, and dsyncAttr() prefers a configured value over the default.
To add a third flavour: extend both, and be honest about what does not carry across:
| Assumption | Where | Applies to |
|---|---|---|
userAccountControl bit 2 = disabled |
DSYNC_UAC_DISABLED, dsyncMapPerson()
|
AD only. A directory without the attribute reports 0, read as enabled β the right default, since we should never invent a reason to deactivate somebody |
objectGUID is binary, entryUUID is text |
ldapStringifyId() in ldap.php
|
Handled, but a third flavour needs checking |
LDAP_CONTROL_PAGEDRESULTS is supported |
dsyncFetchPeople() |
Not universal. A server that ignores it returns its own cap and no error |
manager holds a DN |
dsyncResolveManagers() |
Some directories hold a name or a uid instead β that would need a different second pass, not a different attribute name |
The fixture exists precisely so a third flavour cannot be added blind: docker/ldap-test/ runs Samba AD and OpenLDAP side by side.
dsyncLogEntry($conn, $runId, $action, $userId, $person, $detail).
A new action value needs four things, or it is recorded and never seen:
-
The display order β
get_directory_sync_log.phpsorts by an explicitFIELD(action, β¦)list. An unlisted action sorts first, which is probably not what you want. -
The filter chips β
RUN_ACTIONSinsystem/sso/provider.php, including whether it starts selected. -
Both tenses β
act_will_<action>andact_did_<action>inlang/en/system.php. A preview describes the future and an import describes the past; one label for both states as fact something nobody has agreed to yet. -
A colour β
.act.<action>in the page CSS. Green arrives, amber changes, grey leaves, red needs you.
detailis English prose written at run time, not a translation key. The log is written once and read later, and the run has no user whose locale could render a key.
π΄ It must sit inside a
$previewguard. There are seven. A write that escapes one makes preview lie β and preview is trusted because it is supposed to change nothing, so a lying preview is worse than no preview at all.
if (!$preview) {
$conn->prepare("UPDATE β¦")->execute($args);
}Log the entry outside the guard. What somebody was shown before they pressed the button is worth being able to check afterwards, so a preview records everything it would have done.
Then add an assertion to tests/directory-sync.sh β the existing one is "a preview changes no user at all", comparing three counts before and after.
The four existing ones (see the developer guide Β§4) are independent on purpose: each covers a failure the others cannot see. Before adding a fifth, work out which failure is invisible to all four, because that is the only kind worth adding.
The most recent one came from exactly that question. The brake watches for a sudden drop, so a renamed carve-out β which causes people to be added β was invisible to every existing rule.
Two rules for anything in this area:
- Refuse rather than half-do, when the wrong outcome is destructive. A missing include branch fails the whole run, because importing the branches that do still exist is precisely how everybody in the missing one ends up marked as left.
-
Pair the check with a positive control in the tests. Without one, "no warning was produced" is indistinguishable from "the check never ran".
tests/directory-sync.shproves the brake stops a run and that the same run proceeds with the brake off.
| Tempting | Why not |
|---|---|
| Expressing a carve-out as an LDAP filter | "Not under this subtree" is about position in the tree; a filter tests attributes. It cannot be written |
| Comparing DNs without the trailing comma |
OU=Sales then matches OU=WholesaleSales. Use dsyncDnIsUnder()
|
Storing '' for a person with no email |
users.email is UNIQUE. MySQL tolerates many NULLs and exactly one '', so the second mailbox-less person is rejected as a duplicate |
| Matching people by email across companies | Two companies can legitimately share an address. dsyncFindExisting() scopes it, and that scoping is a tenancy boundary, not a nicety |
| A second preview implementation | Preview runs the same code path with writes suppressed. Two implementations drift, and the one that drifts is the one nobody runs for real |
| Enforcing "no manager cycles" in the database | A self-referencing FK cannot express it. userManagerIsSafe() walks the chain, bounded |
- Directory sync β Developer Guide β how it works
- Directory sync β the design β why, and the questions behind each decision
- Importing people from a directory β the user-facing guide
-
Adding a Language β Developer Guide β for the
map_field_*strings
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
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
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- 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: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- 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
- β³ 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)