-
-
Notifications
You must be signed in to change notification settings - Fork 27
Team Sign In Method Developer Guide
Built for #41 Β· Ships in 2.8.0 Β· User-facing page: Setting the method on a team
A team can carry a default sign-in method, and an analyst set to Follow team takes it. This page covers how that is built, and the one rule that anyone changing teams or analysts must keep.
Any new code that changes an analyst's teams, a team's method or active flag, or an analyst's sign-in method must call analystSignInApplyMany() afterwards. If it doesn't, the analyst keeps signing in the old way. Nothing errors and nothing looks wrong on screen, so the mistake is easy to miss.
The reason is the design decision below.
Four places enforce sign-in, and all of them read a single column, analysts.auth_provider_id:
| Where | What it does with the column |
|---|---|
api/auth/resolve_login.php |
the email-first router sends the analyst to that provider |
auth/login.php |
the password form refuses an analyst pinned to an OIDC provider, and binds against an LDAP one |
api/auth/oidc_callback.php |
strict isolation: the provider that answered must be the one pinned |
includes/ldap.php |
the same check for directory sign-in |
The obvious design is to work out the team default at sign-in time. That would mean teaching all four about teams, and a mistake in any one would be a way round strict isolation, the rule that each account has exactly one way in.
So the team's method is copied into auth_provider_id whenever something that could change it is saved. The four enforcement points were not changed at all. The cost is the rule above: every writer has to keep the copy up to date.
| Column | Meaning |
|---|---|
analysts.auth_follow_team |
1 = take the method from their teams. Defaults to 0, so nobody's sign-in changes on upgrade. The screen defaults new analysts to Follow team. |
teams.auth_method |
NULL = not set, 'local' = password, 'provider' = use teams.auth_provider_id
|
teams.auth_provider_id |
the provider, when auth_method = 'provider'
|
A method is reduced to a single key: 0 for local password, otherwise the provider id. Two teams agree when their keys are equal.
delete_sso_provider.php clears the reference explicitly. The same was true of analysts.auth_provider_id on verify-built tables, and was fixed at the same time.
includes/analyst_signin.php reduces an analyst's active teams' methods to one of three answers:
function analystSignInVerdict(array $teams): array
{
$keys = array_values(array_unique(array_column($teams, 'key')));
if (!$keys) {
return ['status' => 'none', 'key' => null, 'teams' => $teams];
}
if (count($keys) > 1) {
return ['status' => 'conflict', 'key' => null, 'teams' => $teams];
}
return ['status' => 'agreed', 'key' => $keys[0], 'teams' => $teams];
}Only agreed ever writes anything. none and conflict leave the analyst exactly as they were:
function analystSignInApply(PDO $conn, int $analystId): string
{
if (!analystSignInReady($conn)) {
return 'own';
}
$stmt = $conn->prepare("SELECT auth_follow_team, auth_provider_id FROM analysts WHERE id = ?");
$stmt->execute([$analystId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row || (int)$row['auth_follow_team'] !== 1) {
return 'own';
}
$from = analystSignInFromTeams($conn, $analystId);
if ($from['status'] !== 'agreed') {
return $from['status']; // keep what they have - see the header
}
$current = $row['auth_provider_id'] !== null ? (int)$row['auth_provider_id'] : 0;
if ($current === $from['key']) {
return 'unchanged';
}
$conn->prepare("UPDATE analysts SET auth_provider_id = ?, last_modified_datetime = UTC_TIMESTAMP() WHERE id = ?")
->execute([$from['key'] === 0 ? null : $from['key'], $analystId]);
return 'changed';
}Why a conflict keeps the current method rather than picking one. A priority order between teams was the alternative. It was rejected because it's invisible: someone would one day move a person between teams and change how they sign in without meaning to. A conflict is shown to the administrator instead (see The screens), and they decide.
| Endpoint | Who is recalculated |
|---|---|
api/tickets/save_analyst.php |
that analyst, when the form sends auth_follow_team
|
api/tickets/save_analyst_teams.php |
that analyst |
api/tickets/save_team_analysts.php |
members before and after the save |
api/tickets/save_team.php |
the team's members (method and active flag) |
api/tickets/delete_team.php |
the members, read before the delete |
api/system/delete_sso_provider.php |
everyone who follows their team |
Two of those are easy to get wrong.
Members before and after. Someone taken out of a team can change method too, and after the save they are no longer a member to be found:
$before = analystSignInTeamMemberIds($conn, (int)$teamId);
// ... delete and re-insert the team's analyst_teams rows ...
$signin = analystSignInApplyMany($conn, array_merge($before, array_map('intval', $analystIds)));Deleting a team reads the members first, for the same reason. On a verify-built install there's no cascade, so orphan analyst_teams rows can survive the delete. They're harmless, because the lookup joins teams and doesn't find them.
Sign-in itself never changes anyone's method. The JIT account creation in oidc_callback.php and ldap.php sets the provider explicitly and leaves auth_follow_team at 0. Someone created by signing in has their method from that provider, not from a team.
Every one of these endpoints returns signin: {changed, conflict}, and the screens turn that into a message: "Analysts whose sign-in method changed: 2."
New code reaches an install before its administrator runs System β Database Verification. api/tickets/get_analysts.php feeds assignee lists across the whole application, so selecting a column that doesn't exist yet would break far more than this feature. Everything checks analystSignInReady() first:
-
get_analysts.phpadds the team data only for?signin=1, which only System β Analysts sends, and only when the columns are there; -
save_analyst.phpandsave_team.phprefuse Follow team or a team method with "Run System β Database Verificationβ¦", before writing anything; - the screens hide the Follow team option when the data doesn't include
auth_follow_team.
-
System β Teams: a Sign-in method field on the team, a chip showing it, and N sign-in conflicts from
get_teams.php'ssignin_conflictscount. - System β Analysts: Follow team as the first option, and a line under it saying where the answer comes from. It shows either From their team: Keycloak (Service Desk), or the disagreement and the method being kept.
- The Sign-in conflict badge is a button. It opens a modal that draws the analyst branching to each team, with one colour per method, not per team, so teams that agree visibly match. It then says what happens until the conflict is resolved and offers three ways out: Edit (choose a method for them), Teams (make the teams agree) and Change (change their teams).
let analysts in that page is a top-level let, so it is not on window. A test harness driving the page in an iframe has to read it with contentWindow.eval('analysts').
On the docker/proxy-test stack (a throwaway database, never real data), through the real endpoints:
- Before Verification: lists load, and choosing a method gives the message without saving.
-
Ten scenarios:
- joining a team from either side;
- a second team with a different method (conflict kept, both teams flagged);
- teams brought back into agreement;
- a team switched off;
- an analyst with their own method, never touched;
- removal from a team on the team side;
- a provider deleted while a team and an analyst pointed at it;
- a team deleted.
- Enforcement unchanged: with an OIDC method inherited from a team, the password form refused the analyst and the SSO button worked. Set back to local, the password worked.
- Both screens in headless Chrome, with a deliberately broken copy of the page as the negative control.
.btn in every modal, including the page's own Save and Cancel, measures visibility: hidden and is missing from screenshots. That's the test browser, not the page. Inject *{transition:none!important} into the frame before a screenshot.
Not tested: a real sign-in completing at an OIDC provider through a team-inherited method. The redirect was checked; the round trip was not.
- Single sign-on β strict isolation, and the user-facing account of this feature
- LDAP and Active Directory β the directory side of the same column
- System β the Analysts and Teams screens
- Database Verification β developer guide β why the columns have no foreign keys
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: 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
-
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
- β³ π’ 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